English Русский 中文 Deutsch 日本語
preview
Probador de estrategias para Python-MetaTrader 5 (Parte 01): Simulador de operaciones

Probador de estrategias para Python-MetaTrader 5 (Parte 01): Simulador de operaciones

MetaTrader 5Sistemas comerciales |
28 2
Omega J Msigwa
Omega J Msigwa

Contenido


Es mejor hacer algo que no hacer nada mientras se espera a hacerlo todo.

— Winston Churchill.


Introducción

El paquete MetaTrader5-Python es un módulo muy útil que permite a los desarrolladores de Python crear sus propias aplicaciones de trading para la plataforma MetaTrader 5. Otorga a los desarrolladores acceso a la plataforma de negociación para recibir datos, enviar órdenes y supervisar las operaciones.

Este módulo ha revolucionado nuestra forma de concebir la aplicación de escritorio MetaTrader 5; no se trata de una aplicación unidimensional limitada a su lenguaje de programación nativo para la creación de robots de trading, conocido como MQL5. Esta aplicación de negociación es lo suficientemente flexible y capaz de recibir órdenes de negociación desde un lenguaje de programación externo distinto de MQL5.

Aunque el módulo MetaTrader 5 nos permite abrir operaciones en la plataforma MetaTrader 5 mediante Python, carece de una función esencial con la que cuentan todas las aplicaciones de trading basadas en MQL5: la posibilidad de probar una aplicación de trading completamente desarrollada en el Probador de estrategias.

¿Te imaginas poder crear un robot de trading y no poder probarlo? 

Aunque no faltan módulos útiles en Python, ya que hay multitud de módulos, bibliotecas y marcos de trabajo útiles para probar las denominadas estrategias de trading , como Backtrader y Backtesting.py. El problema de estas herramientas basadas en Python es que se crearon para probar estrategias de trading sencillas o, en ocasiones, basadas en indicadores.

Evalúan los resultados de las operaciones basándose únicamente en las señales de trading. No tienen en cuenta todos los aspectos que intervienen en la negociación, como el bróker, las comisiones, las restricciones de la cuenta de negociación, las características de un instrumento concreto (símbolo), el apalancamiento de la cuenta y muchos otros detalles fundamentales que sí tiene en cuenta el Probador de estrategias de MetaTrader 5.

El módulo MetaTrader5-Python tiene como objetivo proporcionar a los usuarios las funciones básicas para obtener información esencial de la aplicación, así como una forma sencilla de dar sus primeros pasos con ella utilizando el lenguaje de programación Python.

Con la información de que disponemos y los conocimientos sobre el funcionamiento del Probador de estrategias de MetaTrader 5, en esta breve serie de artículos vamos a crear y poner en práctica un método práctico (similar al probador de MetaTrader 5) para probar nuestros robots de trading basados en Python.

Empieza instalando todas las dependencias de Python que se encuentran en el archivo llamado requirements.txt, adjunto al final de este artículo.

pip install -r requirements.txt 


Simulador de operaciones 101

Para poder probar estrategias de trading en Python, tenemos que crear un simulador de trading. Esto es similar a lo que hace el Probador de estrategias de MetaTrader 5: simula el mercado y ejecuta una aplicación o unas funciones (un robot de trading o un indicador) durante el proceso. 

No hay que confundirlo: el «Strategy Tester» que ofrece la propia aplicación MetaTrader 5 es un simulador de operaciones.

No vamos a implementar una interfaz gráfica de usuario (GUI) como el Probador de estrategias (al menos por ahora); vamos a crear una clase de Python para esta tarea.

import MetaTrader5 as mt5

class TradeSimulator:
    def __init__(self, simulator_name: str, mt5_instance: mt5, deposit: float, leverage: str="1:100"):
        
        self.mt5_instance = mt5_instance
        self.simulator_name = simulator_name

El objetivo es conseguir un constructor de clase similar a la configuración del Probador de estrategias de MetaTrader 5.

  • La variable mt5_instance es fundamental, ya que permite supervisar una instancia concreta de la aplicación MetaTrader 5. 
  • La variable simulator_name se puede utilizar para crear carpetas y rutas que ayuden a distinguir entre los distintos simuladores de trading; piensa en esta variable como el nombre de un robot de trading (experto o indicador).

En la clase del simulador de operaciones, necesitamos una forma de llevar un seguimiento de la información sobre todas las órdenes abiertas, las posiciones y las posiciones cerradas (deals/ejecuciones), de forma similar a como lo hace MetaTrader 5.

class TradeSimulator:
    def __init__(self, simulator_name: str, mt5_instance: mt5, deposit: float, leverage: str="1:100"):

        # .... other variables
        # ...
        # ...
        
        # Position's information
        
        self.position_info = {
            "time": None,
            "id" : 0,
            "magic": 0,
            "symbol": None,
            "type": None,
            "volume": 0.0,
            "open_price": 0.0,
            "price": 0.0,
            "sl": 0.0,
            "tp": 0.0,
            "commission": 0.0,
            "margin_required": 0.0,
            "fee": 0.0,
            "swap": 0.0,
            "profit": 0,
            "comment": 0
        }
        
        # Order's information
        
        self.order_info = self.position_info.copy()
        self.order_info["expiry_date"] = datetime
        self.order_info["expiration_mode"] = ""
        
        # Deal's information

        self.deal_info = self.position_info.copy()
        
        self.deal_info["reason"] = None # This is used to store the reason why the trade was closed, e.g. "Take Profit", "Stop Loss", etc.
        self.deal_info["direction"] = None # The only difference btn an open trade and a closed one is that the closed one has a direction showing if at that instance it was opened or closed in history
        
        # Containers for positions, orders, and deals
                
        self.positions_container = [] # a list for storing all opened trades
        self.deals_container = [] # a list for storing all deals 
        self.orders_container = []

La tabla siguiente contiene una descripción de las posiciones, las órdenes y la información sobre las operaciones almacenadas en la clase del simulador.

Propiedad Descripción
time El momento en que se ejecuta la operación o la orden. En el caso de una operación, este es el momento de su ejecución (entrada o salida).
id Un identificador único que se incrementa automáticamente de todas las órdenes, posiciones u operaciones.
magic El número mágico de una posición, una orden o una operación.
symbol Un instrumento con el que se abrió la operación, p. ej., (EURUSD, USDJPY)
type El tipo de una posición para las posiciones, el tipo de orden para las órdenes. 
volume Volumen de negociación (tamaño del lote) aplicado a una posición, una orden o una operación.
open_price El precio de apertura de una orden o posición. Puede tratarse tanto del precio de cierre como del precio de apertura de una operación, dependiendo del motivo de la misma.
price  El precio actual del mercado equivale al precio Ask para una posición de compra o órdenes pendientes relacionadas con la compra, y al precio Bid para una posición de venta o órdenes pendientes relacionadas con la venta.
sl El valor del stop loss de una orden, una posición o una operación.
tp El valor de take profit de una orden, una posición o una operación.
comission  Obtiene el importe de la comisión cobrada por una posición.
margin_required  Almacena el margen necesario para que se ejecute dicha posición u orden.
fee  Incluye las comisiones del corredor aplicadas a una posición.
swap Almacena el importe del swap aplicado a una posición.
profit  Almacena las ganancias o pérdidas calculadas de una posición o operación.
comment  Guarda el comentario correspondiente a una posición, una orden o una operación. 
expiration_mode  Almacena SYMBOL_EXPIRATION_MODE para las órdenes pendientes (self.order_info).  
expiry_date Almacena la fecha de caducidad de una orden en formato de hora UTC. 

A continuación, toda la información sobre las posiciones abiertas, las órdenes pendientes y las operaciones ejecutadas se almacena en sus respectivas matrices en un simulador para facilitar el acceso.

        # Containers for positions, orders, and deals
                
        self.positions_container = [] # a list for storing all opened trades
        self.deals_container = [] # a list for storing all deals 
        self.orders_container = [] # for storing all pending orders placed


Cálculo de las ganancias o pérdidas generadas por una posición

El objetivo principal de simular toda la actividad de trading desde el punto de vista de un operador es determinar las ganancias o pérdidas que se podrían haber obtenido utilizando un robot de trading a partir de un momento concreto del pasado.

A continuación se muestra una función universal para esta tarea.

def _calculate_profit(self, action: str, symbol: str, entry_price: float, exit_price: float, lotsize: float) -> float:
    
    """
    Calculate profit based on entry and exit prices, lot size, tick size, and tick value.
    
    Args:
        action (str): The action taken, either 'buy' or 'sell'.
        entry_price (float): The price at which the position was opened.
        exit_price (float): The price at which the position was closed.
        lotsize (float): The size of the lot in terms of contract units.
    """

    if action != "buy" and action != "sell":
        print(f"Unknown order type, It can be either 'buy' or 'sell'. Received '{action}' instead.")
        return 0
    
    order_type = self.mt5_instance.ORDER_TYPE_BUY if action == "buy" else self.mt5_instance.ORDER_TYPE_SELL
    
    profit = self.mt5_instance.order_calc_profit(
        order_type,
        symbol,
        lotsize,
        entry_price,
        exit_price
    )
    
    return profit

Utilizaremos esta función para calcular las ganancias o pérdidas de todas las órdenes de mercado (posiciones) en MetaTrader 5. Indicando el precio de entrada y de salida, un instrumento (símbolo) y el tamaño del lote.

if not mt5.initialize():
    print(f"Failed to Initialize MetaTrader5. Error = {mt5.last_error()}")
    mt5.shutdown()
    quit()

sim = TradeSimulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1000, leverage="1:500")
profit = sim._calculate_profit(action="buy", 
                            symbol="EURUSD", 
                            entry_price=1.17246, 
                            exit_price=1.17390,
                            lotsize=0.07)

print("profit: ", profit)

Resultados.

(pystrategytester) C:\Users\Omega Joctan\OneDrive\Desktop\Python Strategy Tester>conda run --live-stream --name pystrategytester python "c:/Users/Omega Joctan/OneDrive/Desktop/Python Strategy Tester/trade_simulator.py"
profit:  10.08


Simulación de una posición

En una simulación de operaciones comerciales, una posición no es más que un conjunto de datos calculados que se asemejan a una operación y que se almacenan en la memoria o en el disco. 

A continuación se muestra la función básica para abrir posiciones.

    def _open_position(self, pos_type: str, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool:

        trade_info = self.trade_info.copy()

        self.m_symbol.name(symbol)

        self.id += 1  # Increment trade ID

        trade_info["time"] = self.time
        trade_info["id"] = self.id
        trade_info["magic"] = self.magic_number
        trade_info["symbol"] = symbol
        trade_info["type"] = pos_type
        trade_info["volume"] = volume
        trade_info["price"] = price
        trade_info["sl"] = sl
        trade_info["tp"] = tp
        trade_info["commission"] = 0.0
        trade_info["fee"] = 0.0
        trade_info["swap"] = 0.0
        trade_info["profit"] = 0.0
        trade_info["comment"] = comment
        trade_info["margin_required"] = self._calculate_margin(symbol=symbol, volume=volume, price=price)

        # Append to open trades
        self.open_trades_container.append(trade_info)
        print("Trade opened successfully: ", trade_info)

        return True

Una vez más, la propiedad «id», que se asemeja al número de referencia de una posición, se incrementa automáticamente para generar un número de referencia único para cada posición abierta en la instancia de la clase.

La propiedad denominada margin_required ha sido la más complicada de desarrollar hasta ahora porque, aunque el módulo de MetaTrader 5 ofrece una función que ayuda a calcular el margen, esta tiene en cuenta la cuenta con la que se ha iniciado sesión actualmente en la aplicación de MetaTrader 5; utiliza la información de esa cuenta, incluido el apalancamiento.

Dado que queremos una cuenta simulada en este simulador de Python, necesitamos una función personalizada para calcular el valor del margen requerido para cada posición de acuerdo con las credenciales asignadas a la denominada cuenta simulada.

    def _calculate_margin(self, symbol: str, volume: float, open_price: float, margin_rate=1.0) -> float:
        
        """
        Calculates margin requirement similar to MetaTrader5 based on the margin mode.
        """
        self.m_symbol.name(symbol)

        if not self.m_symbol.select():
            print(f"Margin calculation failed: MetaTrader5 error = {self.mt5_instance.last_error()}")
            return 0.0

        contract_size = self.m_symbol.contract_size()
        leverage = self.leverage
        margin_mode = self.m_symbol.trade_calc_mode()

        print("Margin calculation mode: ",self.m_symbol.trade_calc_mode_description())
        
        tick_size = self.m_symbol.tick_size() or 0.0001
        tick_value = self.m_symbol.tick_value() or 0.0
        initial_margin = self.m_symbol.margin_initial() or 0.0
        face_value = self.m_symbol.trade_face_value() 
        
            
        if margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_FOREX:
            margin = (volume * contract_size * margin_rate) / leverage

        elif margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE:
            margin = volume * contract_size * margin_rate

        elif margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_CFD:
            margin = volume * contract_size * open_price * margin_rate

        elif margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_CFDLEVERAGE:
            margin = (volume * contract_size * open_price * margin_rate) / leverage

        elif margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_CFDINDEX:
            margin = volume * contract_size * open_price * tick_value / tick_size * margin_rate

        elif margin_mode in [self.mt5_instance.SYMBOL_CALC_MODE_EXCH_STOCKS, self.mt5_instance.SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX]:
            margin = volume * contract_size * open_price * margin_rate

        elif margin_mode in [self.mt5_instance.SYMBOL_CALC_MODE_FUTURES, 
                             self.mt5_instance.SYMBOL_CALC_MODE_EXCH_FUTURES]:
            
            margin = volume * initial_margin * margin_rate

        elif margin_mode in [self.mt5_instance.SYMBOL_CALC_MODE_EXCH_BONDS, self.mt5_instance.SYMBOL_CALC_MODE_EXCH_BONDS_MOEX]:
            margin = volume * contract_size * face_value * open_price / 100

        elif margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_SERV_COLLATERAL:
            margin = 0.0

        else:
            print(f"Unknown margin mode: {margin_mode}, falling back to default margin calc.")
            margin = (volume * contract_size * open_price) / leverage

        return margin

Aunque la función no es perfecta, teniendo en cuenta que no he podido encontrar la forma de obtener la variable margin_rate desde la aplicación MetaTrader 5, utilizando el módulo MetaTrader5-Python, que parece formar parte de las fórmulas de cálculo de margen utilizadas en MQL5.

Dado que la variable no está disponible en symbol_info, un argumento denominado margin_rate (cuyo valor por defecto es 1,0) nos permite introducir este valor manualmente.

El proceso de almacenar una posición en el contenedor da por hecho que las credenciales facilitadas para dicho puesto no presentan ningún problema. Esto es un gran error porque, como sabemos, la aplicación MetaTrader 5 cuenta con un mecanismo para comprobar si una operación cumple con determinados requisitos relacionados con la cuenta, el símbolo y el bróker antes de aceptarla.

Por ejemplo, la aplicación comprueba si los valores de stop loss y take profit no son demasiado ajustados (cercanos al mercado) y rechaza todas las operaciones que cumplan esta condición; también comprueba si a una operación se le ha asignado un tamaño de posición válido (volumen/tamaño del lote), etc.

Dicho esto, necesitamos una función que devuelva un valor booleano para validar todas las posiciones. Solo se aceptarán las solicitudes que cuenten con todas las credenciales válidas; en caso contrario, serán rechazadas.


Validaciones de operaciones

(a) Validación del tamaño del lote

Para validar el tamaño (volumen) de la operación, comprobamos que se cumplan tres condiciones.

  1. Si el tamaño de un lote determinado es inferior al volumen mínimo aceptado para un símbolo
  2. Si el tamaño de un lote determinado supera el volumen máximo aceptado para un símbolo concreto
  3. Si un tamaño de lote determinado es un múltiplo de su paso (el incremento mínimo de volumen para la ejecución de una operación)

    def _position_validation(self,
                       volume: float,
                       symbol: str,
                       pos_type: str,
                       open_price: float, 
                       sl: float = 0.0, 
                       tp: float = 0.0, 
                       expiry_date: datetime = None) -> bool:
        """
        Validates trade parameters similar to MQL5's OrderCheck()
        
        Returns:
            bool: True if validation passes, False with error message if fails
        """
        
        self.m_symbol.name(symbol) # Assign the current symbol to the CSymbolInfo class for accessing its properties    
            
        # Get symbol properties
        symbol_info = self.m_symbol.get_info() # Get the information about the current symbol
        if symbol_info is None:
            print(f"Trade validation failed. MetaTrader5 error = {self.mt5_instance.last_error()}")
            return False
            
        # Validate volume
        
        if volume < self.m_symbol.lots_min(): # check if the received lotsize is smaller than minimum accepted lot of a symbol
            print(f"Trade validation failed: Volume ({volume}) is less than minimum allowed ({self.m_symbol.lots_min()})")
            return False

        if volume > self.m_symbol.lots_max(): # check if the received lotsize is greater than the maximum accepted lot
            print(f"Trade validation failed: Volume ({volume}) is greater than maximum allowed ({self.m_symbol.lots_max()})")
            return False
        
        step_count = volume / self.m_symbol.lots_step() 
        
        if abs(step_count - round(step_count)) > 1e-7: # check if the stoploss is a multiple of the step size
            print(f"Trade validation failed: Volume ({volume}) must be a multiple of step size ({self.m_symbol.lots_step()})")
            return False
            

(b): Validación del precio de apertura de la operación y comprobación del deslizamiento

Al igual que en el Probador de estrategias de MetaTrader 5, debemos asegurarnos de que una posición tenga un precio de apertura válido antes de aceptarla; es decir, su precio de apertura debe ser muy cercano o igual al precio Ask de un símbolo en el caso de una posición de compra, y debe ser cercano o igual al precio Bid en el caso de una posición de venta.

El valor de deslizamiento (cuando se indica) se utiliza únicamente para comparar precios, con el fin de garantizar que un precio de entrada determinado se aproxime al precio Bid.

        # Validate the opening price
        
        self.m_symbol.refresh_rates() # Get recent ticks information
        
        ask = self.m_symbol.ask()
        bid = self.m_symbol.bid()
        
        if ask is None or bid is None or ask==0 or bid==0:
            print("Trade Validate: Failed to Get Ask and Bid prices, Call the function market_update() to update the simulator with newly simulated price values")
            return False
        
        # Slippage check
        
        actual_price = ask if pos_type == "buy" else bid
        point = self.m_symbol.point()

        # Allowable slippage range (in absolute price)
        
        max_deviation = self.deviation_points * point
        lower_bound = actual_price - max_deviation
        upper_bound = actual_price + max_deviation

        # Check if requested price is within allowed slippage
        
        if not (lower_bound <= open_price <= upper_bound):
            print(f"Trade validation failed: {pos_type.capitalize()} price ({open_price}) is out of slippage range: {lower_bound:.5f} - {upper_bound:.5f}")
            return False

(c): Validación de los niveles de stop loss y take profit

No todos los valores de stop loss y take profit de las órdenes (posiciones) de mercado son aceptables para los brókers de MetaTrader 5; algunos valores de stop loss y take profit podrían no ser válidos o estar demasiado cerca del precio de mercado como para que se abra una posición.

Utilizamos la misma lógica para comprobar tanto el nivel de paradas como el nivel de congelación.

En primer lugar, comprobamos si se ha recibido un stop loss adecuado.

A continuación, nos aseguramos de que el valor del stop loss esté por debajo del precio de apertura de la posición y el del take profit por encima de él, en el caso de una operación de compra. En el caso de una operación de venta, hacemos lo contrario (el stop loss debe estar por encima del precio de apertura y el take profit, por debajo de este).

# Validate stop loss and take profit levels
        
if sl > 0:
    if pos_type == "buy" and sl >= open_price:
        print(f"Trade validation failed: Buy stop loss ({sl}) must be below order opening price ({open_price})")
        return False
    if pos_type == "sell" and sl <= open_price:
        print(f"Trade validation failed: Sell stop loss ({sl}) must be above order opening price ({open_price})")
        return False
    if not self._check_stop_level(symbol, open_price, sl, pos_type):
        return False
                
if tp > 0:
    if pos_type == "buy" and tp <= open_price:
        print(f"Trade validation failed: Buy take profit ({tp}) must be above order opening price ({open_price})")
        return False
    if pos_type == "sell" and tp >= open_price:
        print(f"Trade validation failed: Sell take profit ({tp}) must be below order opening price ({open_price})")
        return False
    if not self._check_stop_level(symbol, open_price, tp, pos_type):
        return False

Las líneas de código anteriores se encuentran dentro de la función denominada _check_stops_level.

    def _check_stop_level(self, symbol: str, price: float, stop_price: float, pos_type: str) -> bool:
        
        """Check if stop levels comply with broker requirements"""
        
        self.m_symbol.name(symbol)
        
        # Validate symbol
        if not self.m_symbol.select():
            print(f"Failed to check stop level: Symbol {symbol}. MetaTrader5 error = {self.mt5_instance.last_error()}")
            return False
        
        # Check for stops level 
        stop_level = self.m_symbol.stops_level()
        
        if pos_type == "buy":
            if stop_price > price - stop_level * self.m_symbol.point():
                print(f"Trade validation failed: Stop level too close. Must be at least {stop_level} points away")
                return False
        else:  # sell
            if stop_price < price + stop_level * self.m_symbol.point():
                print(f"Trade validation failed: Stop level too close. Must be at least {stop_level} points away")
                return False
            
        
        # Check for freeze level
        
        freeze_level = self.m_symbol.freeze_level()
        
        if pos_type == "buy":
            if stop_price > price - freeze_level * self.m_symbol.point():
                print(f"Trade validation failed: Stop level too close. Must be at least {freeze_level} points away")
                return False
        else:  # sell
            if stop_price < price + freeze_level * self.m_symbol.point():
                print(f"Trade validation failed: Stop level too close. Must be at least {freeze_level} points away")
                return False
            
        return True

La función anterior devuelve «False» si se ha detectado un valor no válido de stop loss o take profit en una posición de compra o venta. De lo contrario, devuelve «True».

Por último, llamamos a la función denominada _position_validation dentro de la función base para abrir posiciones. Comprobará la validez de una posición antes de almacenarla en un array que contiene todas las posiciones.

    def _open_position(self, pos_type: str, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool:

        trade_info = self.trade_info.copy()

        self.m_symbol.name(symbol)

        if not self._position_validation(volume=volume, symbol=symbol, pos_type=pos_type, price=price, sl=sl, tp=tp):
            return False

        self.id += 1  # Increment trade ID

        trade_info["time"] = self.time
        trade_info["id"] = self.id
        
        # ... proceeds to store a trade 

        # Append to open trades
        self.open_trades_container.append(trade_info)
        print("Trade opened successfully: ", trade_info)
        
        return True

Para que resulte mucho más cómodo abrir posiciones de compra y venta, he creado dos funciones específicas llamadas buy y sell, destinadas a abrir posiciones de compra y venta, respectivamente. Estas dos funciones se basan en la función base denominada _open_position; la única diferencia entre ambas y su predecesora es una variable denominada pos_type (que sirve para establecer el tipo de posición). Este valor se aplica de forma explícita dentro de las funciones que figuran a continuación.

    def buy(self, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool:
        return self._open_position("buy", volume, symbol, price, sl, tp, comment)

    def sell(self, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool:
        return self._open_position("sell", volume, symbol, price, sl, tp, comment)

Las funciones anteriores se han inspirado en funciones similares disponibles en la clase CTrade que ofrecen las bibliotecas de trading estándar en el lenguaje MQL5.


Modificación de posiciones

Poder modificar tus posiciones es fundamental por diversas razones relacionadas con el trading y la gestión del capital. Por ejemplo, los operadores suelen modificar los valores de los stop loss en sus posiciones, acercándolos a la entrada o al valor de take profit para reducir las pérdidas u obtener beneficios; a esto se le conoce como «trailing stops» o «breakeven».

A continuación se muestra una función destinada a ayudar a los desarrolladores de Python a modificar posiciones en un simulador.

    def position_modify(self, pos: dict, new_sl: float, new_tp) -> bool:
        
        new_position = pos.copy()
        
        if pos["type"] == "buy":
            if new_sl >= pos["price"]: 
                print("Failed to modify sl, new_sl >= current price")
                return False
        
        if pos["type"] == "sell":
            if new_sl <= pos["price"]: 
                print("Failed to modify sl, new_sl <= current price")
                return False
        
        if not self._check_stops_level(symbol=pos["symbol"], open_price=pos["open_price"], stop_price=new_sl, pos_type=pos["type"]):
            print("Failed to Modify the Stoploss")
            
        if not self._check_stops_level(symbol=pos["symbol"], open_price=pos["open_price"], stop_price=new_tp, pos_type=pos["type"]):
            print("Failed to Modify the Takeprofit")
        
        # new sl and tp values 

        new_position["sl"] = new_sl
        new_position["tp"] = new_tp
        
        # Update the position in a container
        
        for i, p in enumerate(self.positions_container):
            if p["id"] == pos["id"]:
                self.positions_container[i] = new_position
                print(f"Position with id=[{pos['id']}] modified! new_sl={new_sl} new_tp={new_tp}")
                return True

        print("Failed to modify position: ID not found")

        return True

El proceso de modificar una posición en MetaTrader 5 presenta algunos aspectos similares a los de abrir una nueva; la función anterior garantiza que se cumplan dos condiciones antes de confirmar la modificación de la posición.

  1. Comprobar si el nuevo stop loss establecido es válido según el tipo de posición; es decir, el nuevo valor del stop loss debe ser superior al precio actual de la posición en el mercado en el caso de una posición de compra, y al contrario en el caso de una posición de venta. 
  2. Asegurarse de que los nuevos valores de stop loss o take profit no estén demasiado cerca del mercado.

Ejemplo de uso:

Abramos una posición de compra sencilla y modifiquemos su stop loss. Cada segundo, aumentamos el stop loss de dicha posición restándole 0,005.

stoploss = 500

ask = m_symbol.ask()
point = m_symbol.point()

sim.buy(volume=0.1, symbol=symbol, open_price=ask, sl=ask-stoploss*point)


while True: # constantly monitor trades and account metrics
    
    sim.monitor_pending_orders()
    sim.monitor_positions(verbose=False)
    
    for pos in sim.get_positions(): # go through all positions, same as in MQL5
        if pos["type"] == "buy" and pos["symbol"] == symbol: # select a buy position for the current symbol
            sim.position_modify(pos=pos, new_sl=pos["sl"]-0.005, new_tp=pos["tp"])   
    
    sim.run_toolbox_gui()  # Run the simulator toolbox GUI
    
    time.sleep(5) # sleep for one second

Resultados.

Position with id=[1] modified! new_sl=1.1320700000000001 new_tp=0.0
Position with id=[1] modified! new_sl=1.1270700000000002 new_tp=0.0
Position with id=[1] modified! new_sl=1.1220700000000003 new_tp=0.0
Position with id=[1] modified! new_sl=1.1170700000000005 new_tp=0.0


Seguimiento de posiciones

Dado que una posición no es más que un conjunto de datos almacenados temporalmente en la memoria, estos datos deben actualizarse constantemente.

Por ejemplo, tras abrir una posición, debemos actualizar sus ganancias o pérdidas actuales en función de las fluctuaciones de los precios en el mercado (los precios de compra y venta más recientes), por no hablar de que debemos supervisar el punto de salida de cada posición, es decir, si el precio actual de mercado (el precio Bid para una posición de compra o el precio Ask para una posición de venta) es igual al stop loss o al take profit de una operación, cerramos dicha operación.

(a): Seguimiento de los beneficios de la actividad comercial

Utilizando la función ya mencionada para calcular las ganancias obtenidas por una posición, supervisamos y actualizamos constantemente las ganancias obtenidas por cada posición.

    def monitor_positions(self, verbose: bool):
        
        # monitoring all open trades
        
        for pos in self.positions_container:
                
            self.m_symbol.name(pos["symbol"])
            self.m_symbol.refresh_rates()
            
            # Get ticks information for every symbol
            
            ask = self.m_symbol.ask()
            bid = self.m_symbol.bid()
            
            # update price information on all positions
            
            pos["price"] = ask if pos["type"] == "buy" else bid
            
            # Monitor and calculate the profit of a position
            
            pos["profit"] = self._calculate_profit(action=pos["type"], symbol=pos["symbol"], lotsize=pos["volume"], entry_price=pos["open_price"], 
                                                    exit_price=(ask if pos["type"]=="buy" else bid))

(b): Seguimiento de las salidas de las posiciones

Una vez que se abre una posición en el simulador de estrategias, con o sin objetivos preestablecidos (valores de stop loss y take profit), esta no se cerrará automáticamente.

Debemos realizar un seguimiento constante comprobando si el precio de mercado actual (precio Ask para una posición de venta y precio Bid para una posición de compra) ha alcanzado ese objetivo deseado. Si se ha alcanzado uno de los objetivos de la posición, esta se cierra y la operación se añade al historial de operaciones.

    def monitor_positions(self, verbose: bool):
        
        # monitoring all open trades
        
        for pos in self.positions_container:
                
            self.m_symbol.name(pos["symbol"])
            self.m_symbol.refresh_rates()
            
            # Get ticks information for every symbol
            
            ask = self.m_symbol.ask()
            bid = self.m_symbol.bid()
            

            # ... other monitors

            
            # Monitor the stoploss and takeprofit situation of positions
            
            if pos["tp"] > 0 and ((pos["type"] == "buy" and bid >= pos["tp"]) or (pos["type"] == "sell" and ask <= pos["tp"])): # Take profit hit    
                self.position_close(pos_id=pos) # close such position
                
            if pos["sl"] > 0 and ((pos["type"] == "buy" and bid <= pos["sl"]) or (pos["type"] == "sell" and ask >= pos["sl"])): # Stop loss hit
                self.position_close(pos_id=pos) # close such position

Por último, queremos mostrar información sobre cada posición a medida que se actualizan, de forma similar a como lo hace la caja de herramientas de la terminal MetaTrader 5 (que nos muestra las posiciones activas).

Solo cuando la variable «verbose» tenga el valor «True».

            # Print the information about all trades (positions and orders (if any))            
            
            if verbose:
                print(f'sim -> ticket | {trade["id"]} | symbol {trade["symbol"]} | time {trade["time"]} | type {trade["type"]} | volume {trade["volume"]} | sl {trade["sl"]} | tp {trade["tp"]} | profit {trade["profit"]:.2f}')

Por ahora, solo estamos supervisando las posiciones de compra y venta; en breve hablaremos también de cómo supervisar las órdenes pendientes.


Órdenes pendientes en el mercado

A diferencia de las órdenes de mercado (posiciones), que están configuradas para su ejecución inmediata en el mercado, las órdenes pendientes consisten en una orden para realizar una operación bursátil cuando se cumpla una determinada condición. Las órdenes pendientes también pueden incluir una restricción temporal en cuanto a su ejecución: la fecha de vencimiento de la orden.

Las órdenes pendientes incluyen:

  1. Buy Limit
  2. Buy Stop
  3. Sell Limit
  4. Sell Stop
  5. Buy Stop Limit
  6. Sell Stop Limit

Por ahora, vamos a implementar los cuatro primeros tipos de órdenes pendientes de la lista anterior en la clase del simulador de operaciones, solo para empezar.

Empezaremos por la función básica para realizar órdenes pendientes.

Las comprobaciones:

(a): Comprobar si el tipo de orden es correcto.

    def _place_a_pending_order(self, 
                               order_type: str,
                               volume: float,
                               symbol: str,
                               open_price: float,
                               sl: float = 0.0,
                               tp: float = 0.0,
                               comment: str = "",
                               expiry_date: datetime = None,
                               expiration_mode: str="gtc"
                               ):
        
        order_types = ["buy limit", "buy stop", "sell limit", "sell stop"]
        
        if order_type not in order_types:
            raise ValueError(f"Invalid pending order type, available order types include: {order_types}")
        
        expiration_modes = ["gtc", "daily", "daily_excluding_stops"]
        if expiration_mode not in expiration_modes:
            raise ValueError(f"Invalid Expiration mode, available modes include: {expiration_modes}")
        

(b): Asegurarse de que ninguna de las órdenes pendientes esté demasiado cerca del precio de mercado

  1. Asegurarse de que el precio de apertura de una orden pendiente de compra no esté demasiado cerca del precio Bid.
  2. Asegurarse de que el precio de apertura de una orden pendiente de venta no esté demasiado cerca del precio Ask.
El valor de SYMBOL_TRADE_STOPS_LEVEL es el que determina la proximidad de una posición al mercado.

# Get market info
        
self.m_symbol.name(symbol_name=symbol) # assign symbol's name
self.m_symbol.refresh_rates() # get recent ticks from the market using the current selected symbol
        
if order_type in ("buy limit", "buy stop"):
            
    if abs(open_price - self.m_symbol.bid()) < self.m_symbol.stops_level() * self.m_symbol.point():
        print(f"Failed to open a pending order, a '{order_type}' order is too close to the market")
        
if order_type in ("sell limit", "sell stop"):
            
    if abs(open_price - self.m_symbol.ask()) < self.m_symbol.stops_level() * self.m_symbol.point():
        print(f"Failed to open a pending order, a '{order_type}' order is too close to the market")

(c): Asegurarse de que se reciba una fecha de vencimiento válida para la orden

La fecha o hora de caducidad debe ser un valor de tiempo superior a la hora actual — una hora en el futuro.

# check if the order has a valid expiry date
        
if expiry_date is not None: # if an expiry date is given in the first place
    if expiry_date <= self.m_symbol.time(timezone=pytz.UTC):
        print(f"Failed to place a pending order {order_type}, Invalid datetime")
        return

Por último, una vez que una orden supera las tres comprobaciones, se añade a la lista de órdenes almacenadas en la clase.

order_info = self.order_info.copy()
        
self.id += 1
        
order_info["id"] = self.id
order_info["type"] = order_type
order_info["volume"] = volume
order_info["symbol"] = symbol
order_info["open_price"] = open_price
order_info["sl"] = sl
order_info["tp"] = tp
order_info["comment"] = comment
order_info["magic"] = self.magic_number
order_info["margin_required"] = self._calculate_margin(symbol=symbol, volume=volume, open_price=open_price)
        
order_info["expiry_date"] = expiry_date
order_info["expiration_mode"] = expiration_mode
        
self.orders_container.append(order_info) # add a valid order to its container

Incrementamos el mismo id (número de ticket) que se utiliza para establecer el identificador de las posiciones, también al colocar órdenes pendientes, ya que una orden pendiente es, en realidad, una posición encubierta (es una posición a la espera de abrirse y todas las posiciones fueron en su momento una orden).

Utilizar el mismo id ayuda a evitar que se dupliquen los números id en las posiciones activadas.

Partiendo de esta función básica, vamos a implementar funciones prácticas e independientes para realizar órdenes pendientes.

Cómo realizar una orden Buy Stop:

    def buy_stop(self, volume: float, symbol: str, open_price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "", expiry_date: datetime = None,expiration_mode: str="gtc"):
        
        # validate an order according to its type
        
        self.m_symbol.name(symbol_name=symbol)
        self.m_symbol.refresh_rates()
        
        if self.m_symbol.bid() >= open_price:
            print("Failed to place a buy stop order, open price <= the bid price")    
            return
        
        self._place_a_pending_order("buy stop", volume, symbol, open_price, sl, tp, comment, expiry_date, expiration_mode)    

Cómo realizar una orden de tipo Buy Limit:

    def buy_limit(self, volume: float, symbol: str, open_price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "", expiry_date: datetime = None, expiration_mode: str="gtc"):
        
        self.m_symbol.name(symbol_name=symbol)
        self.m_symbol.refresh_rates()
        
        if self.m_symbol.bid() <= open_price:
            print("Failed to place a buy limit order, open price >= current bid price")
            return

        self._place_a_pending_order("buy limit", volume, symbol, open_price, sl, tp, comment, expiry_date, expiration_mode)

Cómo realizar una orden de tipo Sell Stop:

    def sell_stop(self, volume: float, symbol: str, open_price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "", expiry_date: datetime = None, expiration_mode: str="gtc"):
        
        self.m_symbol.name(symbol_name=symbol)
        self.m_symbol.refresh_rates()

        if self.m_symbol.ask() <= open_price:
            print("Failed to place a sell stop order, open price >= current ask price")
            return

        self._place_a_pending_order("sell stop", volume, symbol, open_price, sl, tp, comment, expiry_date, expiration_mode)

Cómo realizar una orden de tipo Sell Limit:

    def sell_limit(self, volume: float, symbol: str, open_price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "", expiry_date: datetime = None, expiration_mode: str="gtc"):
        
        self.m_symbol.name(symbol_name=symbol)
        self.m_symbol.refresh_rates()

        if self.m_symbol.ask() >= open_price:
            print("Failed to place a sell limit order, open price <= current ask price")
            return

        self._place_a_pending_order("sell limit", volume, symbol, open_price, sl, tp, comment, expiry_date, expiration_mode)

En las funciones anteriores, añadimos condiciones para garantizar que cada orden se coloque, como mínimo, en el lugar correcto, es decir:

  1. Una orden Buy Stop se coloca por encima del precio actual de mercado (precio Ask).
  2. Una orden Buy Limit se coloca por debajo del precio actual de mercado (precio Bid).
  3. Una orden Sell Stop se coloca por debajo del precio actual de mercado (precio Bid).
  4. Una orden Sell Limit se coloca por encima del precio actual de mercado (precio Ask). 


Eliminación de órdenes pendientes

Contar con una función encargada de eliminar las órdenes pendientes es tan importante como contar con una función para introducirlas.

En esta función no es necesario realizar ninguna comprobación y, una vez eliminada una orden, no se guarda ningún registro.

    def order_delete(self, selected_order: dict) -> bool:
        
        # delete a pending order from the orders container
        
        if selected_order in self.orders_container:
            
            self.orders_container.remove(selected_order)
            return True
        
        else:
            print(f"Warning: An Order with ID {selected_order['id']} not found!")
            return False


Modificación de órdenes pendientes

También necesitamos una función para modificar las órdenes pendientes, de forma similar a como tenemos una función para modificar las posiciones.

Hay tres comprobaciones importantes que hay que realizar dentro de una función para esta tarea.

(a): La comprobación para garantizar que el nuevo precio de apertura de una posición se ajusta correctamente al tipo de orden.

En todas las órdenes pendientes, se debe indicar el nuevo precio de apertura:

  1. Por encima del precio actual de mercado (precio Ask) para una orden Buy Stop
  2. Por debajo del precio de mercado actual (precio Bid) para una orden Buy Limit
  3. Por debajo del precio de mercado actual (precio Bid) para una orden Sell Stop
  4. Por encima del precio actual de mercado (precio Ask) para una orden Sell Limit
    def order_modify(self, order: dict, new_open_price: float, new_sl: float, new_tp: float, new_expiry: datetime = None, new_expiration_mode: str = None):
        """
         Modify an existing pending order's open price, SL/TP, and optionally its expiration settings.
        """
        new_order = order.copy()

        # Validate order type
        valid_types = ["buy limit", "buy stop", "sell limit", "sell stop"]
        if order["type"] not in valid_types:
            print(f"Invalid order type for modification: {order['type']}")
            return False

        self.m_symbol.name(order["symbol"])
        self.m_symbol.refresh_rates()

        # Ensure open price is placed logically according to type
        ask = self.m_symbol.ask()
        bid = self.m_symbol.bid()

        if order["type"] == "buy stop" and bid >= new_open_price:
            print("Failed to modify Buy Stop: new open price <= current bid price")
            return False
        if order["type"] == "buy limit" and bid <= new_open_price:
            print("Failed to modify Buy Limit: new open price >= current bid price")
            return False
        if order["type"] == "sell stop" and ask <= new_open_price:
            print("Failed to modify Sell Stop: new open price >= current ask price")
            return False
        if order["type"] == "sell limit" and ask >= new_open_price:
            print("Failed to modify Sell Limit: new open price <= current ask price")
            return False

(b): La comprobación para asegurarse de que el precio de apertura de la nueva orden no esté muy cerca del precio de mercado.

# ensure the order ins't close to the market
        
order_type = order["type"]
if order_type in ("buy limit", "buy stop"):
            
    if abs(new_open_price - self.m_symbol.bid()) < self.m_symbol.stops_level() * self.m_symbol.point():
        print(f"Failed to open a pending order, a '{order_type}' order is too close to the market")
        return False
        
if order_type in ("sell limit", "sell stop"):
            
    if abs(new_open_price - self.m_symbol.ask()) < self.m_symbol.stops_level() * self.m_symbol.point():
        print(f"Failed to open a pending order, a '{order_type}' order is too close to the market")
        return False

(c): La comprobación para asegurarse de que la fecha de vencimiento de la nueva orden sea la adecuada

if new_expiry and new_expiry <= self.m_symbol.time(timezone=pytz.UTC):
    print("Invalid Expiry date, new expiry date must be a value in the future")

Por último, modificamos y actualizamos todas las órdenes del contenedor.

# Update the order in the container
for i, o in enumerate(self.orders_container):
    if o["id"] == order["id"]:
        self.orders_container[i] = new_order
        print(f"Order with id=[{order['id']}] modified successfully.")
        return True

print("Failed to modify order: ID not found")
return False


Seguimiento de las órdenes pendientes

Al igual que una posición, una orden no es más que un conjunto de datos almacenados en una lista de diccionarios dentro de la clase. Una vez almacenada una orden, debe supervisarse constantemente; es decir, debe existir un código que compruebe si el precio actual (de venta o de compra) ha alcanzado el precio de apertura de una orden pendiente; cuando el precio de mercado actual alcance el precio de apertura de la orden, esta se activa y se añade a una lista de posiciones abiertas.

También debemos supervisar el estado del plazo de vencimiento en todas las órdenes pendientes que tengan una fecha de vencimiento y el modo de vencimiento adecuado, más información.

    def monitor_pending_orders(self):
        
        now = datetime.now(tz=pytz.UTC)
        
        expired_orders = []
        triggered_orders = []

        for order in self.orders_container: # loop through all orders
            
            expiration_mode = order.get("expiration_mode", "gtc")
            expiry_date = order.get("expiry_date")

            # Check for expiration based on mode
            if expiration_mode == "daily" or expiration_mode == "daily_excluding_stops":
                if expiry_date and now >= expiry_date:
                    
                    expired_orders.append(order)
                    continue  # Skip to next order

            self.m_symbol.name(symbol_name=order["symbol"])
            
            if not self.m_symbol.refresh_rates():
                continue

            ask = self.m_symbol.ask()
            bid = self.m_symbol.bid()
            open_price = order["open_price"]
            order_type = order["type"].lower()
            
            if order_type in ("buy limit", "buy stop"):
                order["price"] = self.m_symbol.ask()

            if order_type in ("sell limit", "sell stop"):
                order["price"] = self.m_symbol.bid()
                
            triggered = False # store the triggered condition of an order
            
            if order_type == "buy limit" and ask <= open_price:
                triggered = self.buy(order["volume"], order["symbol"], ask, order["sl"], order["tp"], order["comment"]) # open a buy position with credentials taken from an order

            elif order_type == "buy stop" and ask >= open_price:
                triggered = self.buy(order["volume"], order["symbol"], ask, order["sl"], order["tp"], order["comment"]) # open a buy position

            elif order_type == "sell limit" and bid >= open_price:
                triggered = self.sell(order["volume"], order["symbol"], bid, order["sl"], order["tp"], order["comment"]) # open a sell position

            elif order_type == "sell stop" and bid <= open_price:
                triggered = self.sell(order["volume"], order["symbol"], bid, order["sl"], order["tp"], order["comment"]) # open a sell position

            if triggered:
                triggered_orders.append(order) # add a triggerd order to the list 

        # Clean up expired and triggered orders
        for order in expired_orders + triggered_orders:
            
            if order in self.orders_container:
                self.orders_container.remove(order)


Monitorización de la cuenta

Tras supervisar todas las posiciones y actualizar sus datos (incluidos los valores de ganancias y pérdidas), debemos actualizar también los datos de nuestra cuenta, es decir, el saldo de la cuenta en función del depósito, el capital, el margen, el margen libre y el nivel de margen del simulador; todos estos datos de la cuenta dependen de las actividades de negociación.

Se supervisa una cuenta simulada dentro de una función denominada monitor_account:

Propiedad de la cuenta
Cálculo

Descripción
Cálculo de pérdidas y ganancias acumuladas
unrealized_pl = sum(pos['profit'] or 0 for pos in self.positions_container)
        
self.account_info["profit"] = unrealized_pl
Calcula la suma de los beneficios de todas las posiciones abiertas en el simulador.
Actualización del saldo neto de la cuenta
self.account_info['equity'] = self.account_info['balance'] + unrealized_pl
La equidad de la cuenta es el resultado de la suma de las ganancias y pérdidas de todas las posiciones, una vez restadas del saldo de la cuenta.
Margen utilizado
self.account_info['margin'] = sum(pos['margin_required'] or 0 for pos in self.positions_container)
El margen total utilizado es la suma de los márgenes consumidos por todas las posiciones.
Margen libre
self.account_info['free_margin'] = self.account_info['equity'] - self.account_info['margin']
El margen libre es la diferencia entre el patrimonio neto de la cuenta y el margen total utilizado en la misma.
Nivel de margen
self.account_info['margin_level'] = (self.account_info['equity'] / self.account_info['margin']) * 100 \
            if self.account_info['margin'] > 0 else 0.0
Equivale al patrimonio neto de la cuenta dividido por el margen de la cuenta en porcentaje, únicamente cuando el margen utilizado es mayor que cero (margen > 0).

Por último, mostramos las credenciales de la cuenta al final de la función denominada monitor_account.

Solo cuando el argumento «verbose» se establece en «True».

    def monitor_account(self, verbose: bool):
        
        """Recalculates all account metrics based on current positions"""
        
        # 1. Calculate unrealized P/L
        unrealized_pl = sum(pos['profit'] or 0 for pos in self.open_trades_container)
        
        self.account_info["profit"] = unrealized_pl
        
        # 2. Update Equity (Balance + Floating P/L)
        self.account_info['equity'] = self.account_info['balance'] + unrealized_pl
        
        # 3. Calculate Used Margin
        self.account_info['margin'] = sum(pos['margin_required'] or 0 for pos in self.open_trades_container)
        
        # 4. Calculate Free Margin (Equity - Used Margin)
        self.account_info['free_margin'] = self.account_info['equity'] - self.account_info['margin']
        
        # 5. Calculate Margin Level (Equity / Margin * 100)
        self.account_info['margin_level'] = (self.account_info['equity'] / self.account_info['margin']) * 100 \
            if self.account_info['margin'] > 0 else 0.0
        
        if verbose:
            print(f"Balance: {self.account_info['balance']:.2f} | Equity: {self.account_info['equity']:.2f} | Profit: {self.account_info['profit']:.2f} | Margin: {self.account_info['margin']:.2f} | Free margin: {self.account_info['free_margin']} | Margin level: {self.account_info['margin_level']:.2f}%")

El saldo de la cuenta solo se actualiza cuando se cierra una operación, lo que nos lleva de nuevo a la función denominada position_close.

    def position_close(self, selected_pos: dict) -> bool:

        # Update deal info
        
        deal_info = selected_pos.copy()
        deal_info["direction"] = "closed"
        
        # check if the reason was SL or TP according to recent tick/price information
        
        self.m_symbol.name(selected_pos["symbol"])
        self.m_symbol.refresh_rates()
        
        ask = self.m_symbol.ask()
        bid = self.m_symbol.bid()
        digits = self.m_symbol.digits()
        
        deal_info["reason"] = "Unknown" # Unkown deal reason if the stoploss or takeprofit wasn't hit
        
        if selected_pos["type"] == "buy":
            if np.isclose(selected_pos["tp"], bid, digits): # check if the current bid price is almost equal to the takeprofit
                deal_info["reason"] = "Take profit"           
                
            elif np.isclose(selected_pos["sl"], bid, digits): # check if the current bid price is almost equal to the stoploss
                deal_info["reason"] = "Stop loss"           
        
        
        if selected_pos["type"] == "sell":
            if np.isclose(selected_pos["tp"], ask, digits): # check if the current ask price is almost equal to the takeprofit
                deal_info["reason"] = "Take profit"           
                
            elif np.isclose(selected_pos["sl"], ask, digits): # check if the current ask price is almost equal to the stoploss
                deal_info["reason"] = "Stop loss"               
        
        
        self.deals_container.append(deal_info.copy()) # add the deal to the deals container
        
        print("Trade closed successfully: ", deal_info)
        
        # Save closed deal to database
        self._save_closed_deal(deal_info, self.history_db_name)
        
        # Remove trade from open positions
        
        if selected_pos in self.open_trades_container:
                
            # update the account balance
            self.account_info["balance"] += selected_pos["profit"]
            
            self.open_trades_container.remove(selected_pos)
        else:
            print(f"Warning: Position with ID {selected_pos['id']} not found!")

        return True


Simulación de operaciones en tiempo real en Python

Dada la posibilidad de abrir operaciones y supervisar la actividad de trading desde la clase, TradeSimulator, vamos a abrir nuestras primeras operaciones tanto en la simulación como en la aplicación de escritorio MetaTrader 5. El objetivo es encontrar similitudes entre la actividad de trading en dos entornos distintos.

Antes de abrir las operaciones, debemos tener en cuenta los métodos que se utilizan para configurar los parámetros clave de negociación en un simulador.

class TradeSimulator:
    def __init__(self, simulator_name: str, mt5_instance: mt5, deposit: float, leverage: str="1:100"):

    #... other functions

    def set_magicnumber(self, magic_number: int):
        
        self.magic_number = magic_number
        
    def set_deviation_in_points(self, deviation_points: int):
        
        self.deviation_points = deviation_points

La función denominada set_magicnumber establece el número mágico para todas las operaciones en un simulador, mientras que la función denominada set_deviation_in_points establece el deslizamiento de todas las operaciones de la clase. 

Tras importar todos los módulos necesarios en el archivo simulator_test.py, inicializamos la aplicación de escritorio de MetaTrader 5 mediante el módulo MetaTrader5.

import MetaTrader5 as mt5
from Trade.SymbolInfo import CSymbolInfo
from Trade.Trade import CTrade
from datetime import datetime
import time
import pytz
from trade_simulator import TradeSimulator


if not mt5.initialize(): # Initialize MetaTrader5 instance
    print(f"Failed to Initialize MetaTrader5. Error = {mt5.last_error()}")
    mt5.shutdown()
    quit()

A continuación, se inicializa la clase TradeSimulator.

sim = TradeSimulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500")

magic_number = 123456
slippage = 10

sim.set_magicnumber(magic_number=magic_number) #sets the magic number of a simulator
sim.set_deviation_in_points(deviation_points=slippage) # sets slippage of the simulator

Utilizaremos la clase CTrade que se explica en este artículo para abrir las mismas operaciones en MetaTrader 5; compararemos las operaciones abiertas en un simulador con las abiertas en MetaTrader 5.

m_trade = CTrade() # Initializing the CTrade class

symbol = "EURUSD"

m_trade.set_magicnumber(magic_number=magic_number) # sets the magic number of the CTrade class
m_trade.set_deviation_in_points(deviation_points=slippage) # sets slippage
m_trade.set_filling_type_by_symbol(symbol=symbol) #set filling type by the given symbol

Abrimos las mismas operaciones tanto en el simulador de operaciones como en MetaTrader 5.

m_symbol = CSymbolInfo(mt5_instance=mt5)
m_symbol.name(symbol_name=symbol) # sets the symbol name for the class CSymbolInfo

if m_symbol.refresh_rates() is None: # Get recent ticks data from MetaTrader 5
    print("failed to get recent ticks data")

sim.monitor_account(verbose=True)  # calculate account credentials initially

# Open trades in a Simulator

lotsize = 0.01

if not sim.buy(volume=lotsize, symbol=symbol, open_price=m_symbol.ask(), sl=0.0, tp=0.0, comment="Test Buy Trade"):
    print("Failed to simulate a trade")

if not sim.sell(volume=lotsize, symbol=symbol, open_price=m_symbol.bid(), sl=0.0, tp=0.0, comment="Test Sell Trade"):
    print("Failed to simulate a trade")

# Open trades in MetaTrader5 

if not m_trade.buy(volume=lotsize, symbol=symbol, price=m_symbol.ask(), sl=0.0, tp=0.0, comment="Test Buy Trade"):
    print("Failed to open a trade in MetaTrader5")
    
if not m_trade.sell(volume=lotsize, symbol=symbol, price=m_symbol.bid(), sl=0.0, tp=0.0, comment="Test Buy Trade"):
    print("Failed to open a trade in MetaTrader5")

Es dentro de un bucle infinito donde queremos supervisar todas las posiciones y una cuenta en una simulación.

while True: # constantly monitor trades and account metrics
        
    sim.monitor_account(verbose=True)
    sim.monitor_positions(verbose=True)
    
    time.sleep(1) # sleep for one second

Resultados.

Es muy desagradable de ver. Creemos una sencilla aplicación con interfaz gráfica de usuario (GUI) que nos ayude a visualizar esta actividad de trading en Python.


GUI para simulaciones en tiempo real

Para esta sencilla aplicación, utilizamos el módulo tkinter.

import tkinter as tk
from tkinter import ttk
from datetime import datetime

class SimToolboxGUI:

    def __init__(self):
        self.root = tk.Tk()
        self.root.title("Trade Simulator Monitor")
        self.root.geometry("900x700")
        self.root.configure(bg="#f0f0f0")

        # === ACCOUNT INFO DISPLAY ===
        self.account_label = tk.Label(
            self.root,
            text="",
            font=("Courier", 8),
            anchor="w",
            justify="left",
            bg="#f0f0f0",
            fg="#333",
        )
        self.account_label.pack(fill="x", padx=5, pady=(5, 6))

        # === POSITION TABLE ===
        position_frame = tk.LabelFrame(self.root, text="Open Positions", bg="#f0f0f0")
        position_frame.pack(fill="both", expand=True, padx=10, pady=5)

        self.position_columns = [
            "id", "symbol", "time", "type", "volume", "open_price", "sl", "tp",
            "swap", "price", "profit", "comment"
        ]

        self.position_tree = ttk.Treeview(position_frame, columns=self.position_columns, show="headings", height=10)
        for col in self.position_columns:
            self.position_tree.heading(col, text=col)
            self.position_tree.column(col, anchor="center", width=80)
        self.position_tree.pack(fill="both", expand=True, padx=5, pady=5)

        vsb1 = ttk.Scrollbar(position_frame, orient="vertical", command=self.position_tree.yview)
        self.position_tree.configure(yscrollcommand=vsb1.set)
        vsb1.pack(side="right", fill="y")

        # === ORDER TABLE ===
        order_frame = tk.LabelFrame(self.root, text="Pending Orders", bg="#f0f0f0")
        order_frame.pack(fill="both", expand=True, padx=10, pady=5)

        self.order_columns = [
            "id", "symbol", "time", "type", "volume", "open_price", "sl", "tp", "price",
            "expiry_date", "expiration_mode", "comment"
        ]

        self.order_tree = ttk.Treeview(order_frame, columns=self.order_columns, show="headings", height=10)
        for col in self.order_columns:
            self.order_tree.heading(col, text=col)
            self.order_tree.column(col, anchor="center", width=100)
        self.order_tree.pack(fill="both", expand=True, padx=5, pady=5)

        vsb2 = ttk.Scrollbar(order_frame, orient="vertical", command=self.order_tree.yview)
        self.order_tree.configure(yscrollcommand=vsb2.set)
        vsb2.pack(side="right", fill="y")

    def update(self, account_info: dict, positions: list, orders: list):
        # === Update account info ===
        acc_text = (
            f"Balance: {account_info['balance']:.2f} | "
            f"Equity: {account_info['equity']:.2f} | "
            f"Profit: {account_info['profit']:.2f} | "
            f"Margin: {account_info['margin']:.2f} | "
            f"Free margin: {account_info['free_margin']:.5f} | "
            f"Margin level: {account_info['margin_level']:.2f}%"
        )
        self.account_label.config(text=acc_text)

        # === Refresh positions ===
        for row in self.position_tree.get_children():
            self.position_tree.delete(row)

        for pos in positions:
            row = [pos.get(col, "") for col in self.position_columns]
            self.position_tree.insert("", "end", values=row)

        # === Refresh orders ===
        for row in self.order_tree.get_children():
            self.order_tree.delete(row)

        for order in orders:
            row = []
            for col in self.order_columns:
                val = order.get(col, "")
                if isinstance(val, datetime):
                    val = val.strftime("%Y-%m-%d %H:%M:%S")
                row.append(val)
            self.order_tree.insert("", "end", values=row)

        self.root.update()

    def run(self):
        self.root.mainloop()

La clase anterior crea dos tablas: una para mostrar las órdenes y otra para las posiciones. Además de la interfaz gráfica de usuario, añadimos información sobre la cuenta.

Dentro de la clase TradeSimulator, inicializamos esta interfaz gráfica de usuario de Simulation ToolBox en el constructor de la clase.

Dentro de trade_simulator.py

from toolbox_gui import SimToolboxGUI

class TradeSimulator:
    def __init__(self, simulator_name: str, mt5_instance: mt5, deposit: float, leverage: str="1:100"):

        # ... other variables

        self.toolbox_gui = SimToolboxGUI()  # Initialize the GUI

Creamos una función independiente para actualizar los datos que se muestran en la aplicación GUI.

class TradeSimulator:
    
    # ... other functions               

    def run_toolbox_gui(self):
        
        """
        Runs the simulator toolbox GUI.
        """
        
        self.toolbox_gui.update(self.account_info, self.open_trades_container)

Tras llamar a las funciones de supervisión y regulación de posiciones, órdenes y la cuenta. Llamamos a la función para actualizar la aplicación GUI.

while True: # constantly monitor trades and account metrics
    
    sim.monitor_account(verbose=False)
    sim.monitor_positions(verbose=False)
    sim.monitor_orders()
    
    sim.run_toolbox_gui()  # Run the simulator toolbox GUI
    
    time.sleep(1) # sleep for one second

Una vez más, abramos algunas posiciones y órdenes tanto en MetaTrader 5 como en el simulador de Python, y luego observemos los resultados en ambos.

Nombre del archivo: simulator_test.py

if not mt5.initialize(): # Initialize MetaTrader5 instance
    print(f"Failed to Initialize MetaTrader5. Error = {mt5.last_error()}")
    mt5.shutdown()
    quit()


sim = TradeSimulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500")

magic_number = 123456
slippage = 10

sim.set_magicnumber(magic_number=magic_number) #sets the magic number of a simulator
sim.set_deviation_in_points(deviation_points=slippage) # sets slippage of the simulator

m_trade = CTrade() # Initializing the CTrade class

symbol = "EURUSD"

m_trade.set_magicnumber(magic_number=magic_number) # sets the magic number of the CTrade class
m_trade.set_deviation_in_points(deviation_points=slippage) # sets slippage
m_trade.set_filling_type_by_symbol(symbol=symbol) #set filling type by the given symbol

m_symbol = CSymbolInfo(mt5_instance=mt5)
m_symbol.name(symbol_name=symbol) # sets the symbol name for the class CSymbolInfo


# Open trades in a Simulator

sim.monitor_account(verbose=False)

if m_symbol.refresh_rates() is None: # Get recent ticks data from MetaTrader5
    print("failed to get recent ticks data")
    
# Market Orders

sim.buy(volume=0.1, symbol=symbol, open_price=m_symbol.ask())
sim.sell(volume=0.1, symbol=symbol, open_price=m_symbol.bid())

m_trade.buy(volume=0.1, symbol=symbol, price=m_symbol.ask())
m_trade.sell(volume=0.1, symbol=symbol, price=m_symbol.bid())

# Pending Orders

expiry = datetime.now(tz=pytz.UTC) + timedelta(days=1) # expiration date for pending orders
price_gap = 0.0005

# Buy Stop: place above current ask
sim.buy_stop(volume=0.1, symbol=symbol, open_price=m_symbol.ask() + price_gap, sl=0.0, tp=0.0,
             comment="Buy Stop Example", expiry_date=expiry, expiration_mode="daily")

m_trade.buy_stop(volume=0.1, symbol=symbol, price=m_symbol.ask() + price_gap)

# Buy Limit: place below current bid
sim.buy_limit(volume=0.1, symbol=symbol, open_price=m_symbol.bid() - price_gap, sl=0.0, tp=0.0,
              comment="Buy Limit Example", expiry_date=expiry, expiration_mode="daily_excluding_stops")

m_trade.buy_limit(volume=0.1, symbol=symbol, price=m_symbol.bid() - price_gap)

# Sell Stop: place below current bid

sim.sell_stop(volume=0.1, symbol=symbol, open_price=m_symbol.bid() - price_gap, sl=0.0, tp=0.0,
              comment="Sell Stop Example", expiry_date=expiry, expiration_mode="gtc")

m_trade.sell_stop(volume=0.1, symbol=symbol, price=m_symbol.ask() - price_gap)

# Sell Limit: place above current ask
sim.sell_limit(volume=0.1, symbol=symbol, open_price=m_symbol.ask() + price_gap, sl=0.0, tp=0.0,
               comment="Sell Limit Example", expiry_date=expiry, expiration_mode="gtc")

m_trade.sell_limit(volume=0.1, symbol=symbol, price=m_symbol.bid() + price_gap)

while True: # constantly monitor trades and account metrics
    
    sim.monitor_account()
    sim.monitor_pending_orders()
    sim.monitor_positions(verbose=False)
    sim.monitor_orders()
    
    sim.run_toolbox_gui()  # Run the simulator toolbox GUI
    
    time.sleep(1) # sleep for one second

Resultados.

Genial, los resultados de nuestras operaciones simuladas no son muy cercanos a los resultados reales, pero tampoco están tan alejados, eso ya es un gran avance.


Gestión y control de posiciones y órdenes de forma externa

Es fundamental poder obtener información sobre las posiciones abiertas y gestionarlas fuera del simulador; en eso consiste el trading algorítmico.

Por ejemplo, muchas estrategias de negociación requieren conocer las posiciones abiertas anteriormente. Por ejemplo, una estrategia de negociación podría exigir que un robot abra una posición de compra únicamente si no existe ya una posición en esa dirección y con ese instrumento.

Dicho esto, a continuación se muestra una tabla con funciones que te permiten acceder a todas las órdenes, posiciones y operaciones fuera de la clase denominada TradeSimulator.

Función Devoluciones
def get_positions(self) -> list:
Devuelve todas las posiciones abiertas de un contenedor.
def get_orders(self) -> list:
Devuelve todas las órdenes pendientes de un contenedor.
def get_deals(self, start_time: datetime = None, end_time: datetime = None, from_db: bool = False) -> list
Devuelve todas las operaciones realizadas entre un intervalo de tiempo específico, determinado por las dos variables (start_time y end_time).
Una variable opcional denominada from_db permite decidir si se seleccionan las ofertas almacenadas temporalmente en la memoria o las de la base de datos.

Ejemplo de uso:

sim.buy(volume=0.1, symbol=symbol, open_price=m_symbol.ask())
sim.sell(volume=0.1, symbol=symbol, open_price=m_symbol.bid())

price_gap = 0.0005
# Buy Stop: place above current ask
sim.buy_stop(volume=0.1, symbol=symbol, open_price=m_symbol.ask() + price_gap)


print("Positions total: ",len(sim.get_positions()))
print("Orders total: ",len(sim.get_orders()))

now = m_symbol.time(timezone=pytz.UTC)
start_time = now - timedelta(minutes=5)
end_time = now

print("Deals total: ",len(sim.get_deals(start_time=start_time,
                                        end_time=end_time,
                                        from_db=False
                              )))

Resultados.

(pystrategytester) C:\Users\Omega Joctan\OneDrive\Desktop\Python Strategy Tester>conda run --live-stream --name pystrategytester python "c:/Users/Omega Joctan/OneDrive/Desktop/Python Strategy 
Tester/simulator_test.py"
Trade opened successfully:  {'time': datetime.datetime(2025, 7, 31, 9, 59, 51, tzinfo=<UTC>), 'id': 1, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14597, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Trade opened successfully:  {'time': datetime.datetime(2025, 7, 31, 9, 59, 51, tzinfo=<UTC>), 'id': 2, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'sell', 'volume': 0.1, 'open_price': 1.14589, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Margin calculation mode:   Calculation of profit and margin for Forex
Positions total:  2
Orders total:  1
Deals total:  2

A la hora de seleccionar las operaciones, debes utilizar la hora del símbolo en UTC, la misma que utilizamos al abrir posiciones y órdenes, en lugar de la hora local actual, para evitar discrepancias horarias.

Estas funciones nos permitirán, por tanto, incorporar condiciones específicas a nuestras estrategias de negociación.

(a): Comprobar si existe un tipo de operación concreto en una simulación

Esto es muy habitual a la hora de supervisar las operaciones. En algunas estrategias de negociación, a menudo queremos abrir determinadas posiciones y órdenes solo cuando aún no existen.

if not mt5.initialize(): # Initialize MetaTrader5 instance
    print(f"Failed to Initialize MetaTrader5. Error = {mt5.last_error()}")
    mt5.shutdown()
    quit()

sim = TradeSimulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500")

magic_number = 123456
slippage = 10

sim.set_magicnumber(magic_number=magic_number) #sets the magic number of a simulator
sim.set_deviation_in_points(deviation_points=slippage) # sets slippage of the simulator

symbol = "EURUSD"
m_symbol = CSymbolInfo(mt5_instance=mt5)
m_symbol.name(symbol_name=symbol) # sets the symbol name for the class CSymbolInfo


def is_position_exists(type: str) -> bool:
    
    for pos in sim.get_positions():
        if pos["magic"] == magic_number and pos["symbol"] == symbol and pos["type"] == type:
            return True # position exists
        
    return False
    
while True: #imitating the OnTick function offered in MQL5 language
    
    sim.monitor_pending_orders()
    sim.monitor_positions(verbose=False)
    sim.monitor_account(verbose=False)
    
    sim.run_toolbox_gui()  # Run the simulator toolbox GUI
    
    if m_symbol.refresh_rates() is None: # Get recent ticks data from MetaTrader5
        # print("failed to get recent ticks data")
        continue
        
    if not is_position_exists("buy"): # open a buy trade in a simulator if it doesn't exist
        sim.buy(volume=0.1, symbol=symbol, open_price=m_symbol.ask())
    
    if not is_position_exists("sell"): # open a sell trade in a simulator if it doesn't exist
        sim.sell(volume=0.1, symbol=symbol, open_price=m_symbol.bid())
    
    time.sleep(1) # sleep for one second    

Resultados.

(pystrategytester) C:\Users\Omega Joctan\OneDrive\Desktop\Python Strategy Tester>conda run --live-stream --name pystrategytester python "c:/Users/Omega Joctan/OneDrive/Desktop/Python Strategy 
Tester/simulator_test.py"
Trade opened successfully:  {'time': datetime.datetime(2025, 7, 31, 10, 13, 18, tzinfo=<UTC>), 'id': 1, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14565, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Trade opened successfully:  {'time': datetime.datetime(2025, 7, 31, 10, 13, 18, tzinfo=<UTC>), 'id': 2, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'sell', 'volume': 0.1, 'open_price': 1.14557, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}

Solo se abrieron dos posiciones distintas (posiciones de compra y de venta).

Se trata de una interfaz similar a la que ofrece MQL5, que solemos utilizar para comprobar si existe una posición determinada.

(b): Cierre de una posición concreta

def close_positions(type: str):
    
    for pos in sim.get_positions():
        if pos["magic"] == magic_number and pos["symbol"] == symbol and pos["type"] == type:
            sim.position_close(pos)

Es posible que algunas estrategias requieran cerrar operaciones concretas cuando se cumpla una condición programada específica; en ese sentido, la función anterior o un enfoque similar resultan muy útiles.

Abramos dos posiciones (una de compra y otra de venta) y cerremos la posición de compra.

while True:
    
    sim.monitor_pending_orders()
    sim.monitor_positions(verbose=False)
    sim.monitor_account(verbose=False)
    
    sim.run_toolbox_gui()  # Run the simulator toolbox GUI
    
    if m_symbol.refresh_rates() is None: # Get recent ticks data from MetaTrader5
        # print("failed to get recent ticks data")
        continue
        
    if not is_position_exists("buy"): # open a buy trade in a simulator if it doesn't exist
        sim.buy(volume=0.1, symbol=symbol, open_price=m_symbol.ask())
    
    close_positions("buy") # close all buy positions
    
    if not is_position_exists("sell"): # open a sell trade in a simulator if it doesn't exist
        sim.sell(volume=0.1, symbol=symbol, open_price=m_symbol.bid())
    
    time.sleep(1) # sleep for one second    

Resultados.

(pystrategytester) C:\Users\Omega Joctan\OneDrive\Desktop\Python Strategy Tester>conda run --live-stream --name pystrategytester python "c:/Users/Omega Joctan/OneDrive/Desktop/Python Strategy 
Tester/simulator_test.py"
Trade opened successfully:  {'time': datetime.datetime(2025, 7, 31, 10, 50, 35, tzinfo=<UTC>), 'id': 1, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14447, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Trade closed successfully:  {'time': datetime.datetime(2025, 7, 31, 10, 50, 35, tzinfo=<UTC>), 'id': 1, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14447, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': '', 'direction': 'closed', 'reason': 'Take profit'}        
Trade opened successfully:  {'time': datetime.datetime(2025, 7, 31, 10, 50, 35, tzinfo=<UTC>), 'id': 2, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'sell', 'volume': 0.1, 'open_price': 1.14439, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Trade opened successfully:  {'time': datetime.datetime(2025, 7, 31, 10, 50, 37, tzinfo=<UTC>), 'id': 3, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14446, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Trade closed successfully:  {'time': datetime.datetime(2025, 7, 31, 10, 50, 37, tzinfo=<UTC>), 'id': 3, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14446, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': '', 'direction': 'closed', 'reason': 'Take profit'}        


Trabajar con deals/ejecuciones

En MetaTrader 5, una operación representa la ejecución efectiva de una operación: es el resultado de una orden. Cada operación se basa en una orden concreta, pero una sola orden puede dar lugar a varias operaciones (por ejemplo, si la orden se ejecuta por partes).

Las operaciones se crean cuando...

  1. Se abre una posición,
  2. Una posición se cierra parcial o totalmente,
  3. O bien se activa y se ejecuta una orden (como una orden limitada o una orden stop).

En otras palabras, tanto las ejecuciones de entrada como las de salida se registran como operaciones.

A diferencia de las órdenes y las posiciones, que pueden modificarse temporalmente, las operaciones son inmutables y siempre se guardan en el historial de operaciones. Sirven como registro permanente de las operaciones realizadas y no se pueden modificar ni eliminar.

Al final de las funciones _position_open y position_close, que abren y cierran posiciones respectivamente, se añade una operación a una lista denominada deals_container que se encuentra en el constructor de la clase.

    def _open_position(self, pos_type: str, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool:

        trade_info = self.trade_info.copy()

        # ... other operations
        # ...

        # Append to open trades
        self.open_trades_container.append(trade_info)
        print("Trade opened successfully: ", trade_info)

        # Track deal
        self.deal_info.update(trade_info)
        self.deal_info["direction"] = "opened"
        self.deal_info["reason"] = "Expert"
        self.deals_container.append(self.deal_info.copy())
    def position_close(self, selected_pos: dict) -> bool:

        # Update deal info
        
        deal_info = selected_pos.copy()
        deal_info["direction"] = "closed"
        
        # ... other operations
        
        deal_info["reason"] = "Unknown" # Unkown deal reason if the stoploss or takeprofit wasn't hit
        
        if selected_pos["type"] == "buy":
            if np.isclose(selected_pos["tp"], bid, digits): # check if the current bid price is almost equal to the takeprofit
                deal_info["reason"] = "Take profit"           
                
            elif np.isclose(selected_pos["sl"], bid, digits): # check if the current bid price is almost equal to the stoploss
                deal_info["reason"] = "Stop loss"           
        
        
        if selected_pos["type"] == "sell":
            if np.isclose(selected_pos["tp"], ask, digits): # check if the current ask price is almost equal to the takeprofit
                deal_info["reason"] = "Take profit"           
                
            elif np.isclose(selected_pos["sl"], ask, digits): # check if the current ask price is almost equal to the stoploss
                deal_info["reason"] = "Stop loss"               
        
        
        self.deals_container.append(deal_info.copy()) # add the deal to the deals container
        
        print("Trade closed successfully: ", deal_info)

Sin embargo, guardar las operaciones abiertas por el simulador en una lista o matriz no es lo ideal, ya que esta información se perderá en cuanto se cierre el programa. Almacenémoslos en una base de datos SQLite3 y hagamos que su registro sea permanente, a menos que se modifiquen o se eliminen, al igual que lo hace Metatrader 5.

    def _create_deals_db(self, db_name: str):
        
        """
         Creates a SQLite database to store trade history and account information.
        
        Args:
            db_name (str): The name of the database file.
        """
        
        conn = sqlite3.connect(db_name)
        cursor = conn.cursor()

        # Create tables if they do not exist
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS closed_deals (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                time TEXT,
                magic INTEGER,
                symbol TEXT,
                type TEXT,
                direction TEXT,
                volume REAL,
                price REAL,
                sl REAL,
                tp REAL,
                commission REAL,
                margin_required REAL,
                fee REAL,
                swap REAL,
                profit REAL,
                comment TEXT,
                reason TEXT
            )
        ''')
        
        conn.commit() 
        conn.close()

La función anterior se invoca dentro de la clase TradeSimulator, en el constructor.

class TradeSimulator:
    def __init__(self, simulator_name: str, mt5_instance: mt5, deposit: float, leverage: str="1:100"):
        
        # ... other variables
        # ...

        # Database for trade history
        
        self.sim_folder = "Simulations"
        
        os.makedirs(self.sim_folder, exist_ok=True)  # Ensure the simulations path exists
        
        # Create the database file name
        
        self.history_db_name = os.path.join(self.sim_folder, self.simulator_name+".db")
        self._create_deals_db(self.history_db_name)

Tras crear una base de datos con un nombre similar al del simulador, indicado por la variable simulator_name,, la función denominada _create_deals_db crea una tabla llamada closed_deals si aún no existe.

También necesitamos una función para guardar cada operación en la base de datos.

    def _save_deal(self, deal: dict, db_name: str):
        """
            Saves a closed deal to the SQLite database.
        """
        
        conn = sqlite3.connect(db_name)
        cursor = conn.cursor()

        cursor.execute("""
            INSERT INTO closed_deals (
                time, magic, symbol, type, direction, volume, price, sl, tp,
                commission, margin_required, fee, swap, profit, comment, reason
            ) VALUES (
                :time, :magic, :symbol, :type, :direction, :volume, :price, :sl, :tp,
                :commission, :margin_required, :fee, :swap, :profit, :comment, :reason
            );
        """, deal)

        conn.commit()
        conn.close()

¿Te has fijado en que no estamos añadiendo una columna llamada id a la base de datos? Esto se debe a que la columna denominada «id» de la tabla de la base de datos está configurada como AUTOINCREMENT para garantizar que a cada operación se le asigne un identificador único a lo largo de todo el historial, desde 0 hasta el infinito positivo.

Tenemos que guardar todas las operaciones en la base de datos dentro de las funciones encargadas de abrir y cerrar posiciones, tras almacenarlas en la lista denominada deals_container.

Dentro de la función denominada position_close.

    def position_close(self, selected_pos: dict) -> bool:

        # Update deal info
        
        deal_info = selected_pos.copy()
        deal_info["direction"] = "closed"
        
        
        #... 
        #...

        print("Trade closed successfully: ", deal_info)
        
        # Save closed deal to database
        self._save_deal(deal_info, self.history_db_name)

Dentro de la función denominada _open_position.

    def _open_position(self, pos_type: str, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool:

        trade_info = self.trade_info.copy()

        #...
        #...
        #...

        self.deals_container.append(self.deal_info.copy())

        # Log to database
        self._save_deal(self.deal_info, self.history_db_name)

        return True

A continuación se muestra la base de datos SQLite que contiene todas las transacciones realizadas en las últimas horas y días.


Reflexiones finales

Mientras implemento esta primera parte del simulador de MetaTrader 5, no puedo evitar admirar lo sofisticado que es el Probador de estrategias de MetaTrader 5. En segundo plano, esta herramienta realiza muchas otras tareas además de simplemente ejecutar operaciones.

Llegados a este punto, quizá te estés preguntando: «¿Es necesario este simulador?». ¿Es porque hemos implementado un simulador de operaciones que abre operaciones en lo que parece una cuenta real, algo que no difiere de lo que hace la aplicación MetaTrader 5 cuando se utiliza el módulo MetaTrader5-Python?

El objetivo de este artículo era comprender la dinámica de un simulador de operaciones; al simular algunas operaciones sencillas y asegurarnos de que sean muy similares a las que se abren en una cuenta real, podemos decir que nos estamos acercando a nuestro objetivo.

También es justo decir que este simulador dista mucho de ser completo o perfecto en comparación con el Probador de estrategias de la plataforma MetaTrader 5. Todavía hay muchas cosas que faltan o que no están bien hechas; la verdad es que resulta complicado estar al tanto de todos los detalles, así que, si tienes alguna idea u opinión, o si quieres colaborar en el proyecto, aquí tienes el enlace al repositorio de GitHub -> https://github.com/MegaJoctan/PyMetaTester.

¿Y ahora qué?

En el simulador de operaciones mencionado anteriormente, hemos extraído información clave del mercado, como los precios actuales de compra y venta, junto con otros datos importantes sobre el valor seleccionado. En los próximos artículos, analizaremos diferentes formas de extraer datos de ticks y de procesar esta información en un bucle para imitar el comportamiento de las pruebas históricas del Probador de estrategias.

Hasta la próxima.


Tabla de archivos adjuntos

Nombre del archivo Descripción y uso
requirements.txt Contiene todas las dependencias de Python utilizadas en este proyecto.
trade_simulator.py Cuenta con la clase TradeSimulator, que alberga todo el simulador de operaciones.
simulator_test.py Un script de prueba para probar el simulador de operaciones del que se ha hablado.
toolbox_gui.py Incluye una aplicación con una interfaz gráfica de usuario sencilla, similar a la de MetaTrader 5, para mostrar la información sobre las operaciones y el saldo de la cuenta.
Trade\SymbolInfo.py Contiene la clase denominada CSymbolInfo, que proporciona toda la información de MetaTrader 5 sobre un símbolo concreto.
Trade\Trade.py  Contiene la clase denominada CTrade, que ofrece funciones para abrir posiciones y órdenes en MetaTrader 5 mediante el módulo metatrader5-Python. 

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/18971

Archivos adjuntos |
Attachments.zip (17.05 KB)
Anton du Plessis
Anton du Plessis | 10 ago 2025 en 10:19
Gracias por tu labor pionera. Tengo muchas ganas de probarlo.
Joern Ulf Rechenburg
Joern Ulf Rechenburg | 9 jun 2026 en 13:48
Vaya, qué guay. Yo también lo voy a probar. Gracias por compartirlo.
Del nivel básico al intermedio: Subventanas (II) Del nivel básico al intermedio: Subventanas (II)
El artículo profundiza en el uso de las subventanas en MetaTrader 5 y muestra cómo la dirección del cálculo en OnCalculate afecta a los búferes y al trazado de las medias. Explica, en la práctica, cómo se encadenan indicadores mediante FIRST INDICATOR’S DATA y PREVIOUS INDICATOR’S DATA, el efecto de la eliminación en cadena de los indicadores dependientes y cómo se comporta indicatorseparatewindow. El lector aprenderá a diagnosticar problemas de visualización y a estructurar indicadores que funcionen correctamente en subventanas.
Red neuronal en la práctica: Cuándo usar una neurona artificial y entender su función en MQL5 Red neuronal en la práctica: Cuándo usar una neurona artificial y entender su función en MQL5
Implementamos en MQL5 una neurona entrenada mediante descenso de gradiente estocástico y comparamos su función de costo con la regresión lineal. Mostramos, mediante código y gráficos, cómo la normalización, la elección de la tasa de aprendizaje y la estructura del problema afectan a la convergencia. El artículo ofrece una guía para depurar el entrenamiento, interpretar el comportamiento del error y seleccionar la arquitectura o la función de activación adecuada.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Del nivel básico al intermedio: Subventanas (I) Del nivel básico al intermedio: Subventanas (I)
En este artículo comenzaremos a ver cómo trabajar con subventanas en MetaTrader 5 mediante MQL5. Es un tema amplio, con varios aspectos prácticos que pueden resultar complicados, así que haremos una breve introducción. Aun así, querido lector, es importante que comprendas lo que veremos aquí, pues podría marcar una gran diferencia en tu futuro.