Probador de estrategias para Python-MetaTrader 5 (Parte 01): Simulador de operaciones
Contenido
- Introducción
- Simulador de trading 101
- Cálculo de las ganancias/pérdidas generadas por una posición
- Simulación de una posición
- Validaciones de trading
- Modificación de posiciones
- Seguimiento de posiciones
- Órdenes pendientes del mercado
- Eliminación de órdenes pendientes
- Modificación de órdenes pendientes
- Seguimiento de órdenes pendientes
- Monitorización de la cuenta
- Simulación de operaciones en tiempo real en Python
- Aplicación gráfica de interfaz de usuario para simulaciones en tiempo real
- Gestión y control de posiciones y órdenes de forma externa
- Trabajar con deals/ejecuciones
- Conclusión
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.
- Si el tamaño de un lote determinado es inferior al volumen mínimo aceptado para un símbolo
- Si el tamaño de un lote determinado supera el volumen máximo aceptado para un símbolo concreto
- 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.
- 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.
- 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:
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
- Asegurarse de que el precio de apertura de una orden pendiente de compra no esté demasiado cerca del precio Bid.
- Asegurarse de que el precio de apertura de una orden pendiente de venta no esté demasiado cerca del precio Ask.
# 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:
- Una orden Buy Stop se coloca por encima del precio actual de mercado (precio Ask).
- Una orden Buy Limit se coloca por debajo del precio actual de mercado (precio Bid).
- Una orden Sell Stop se coloca por debajo del precio actual de mercado (precio Bid).
- 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:
- Por encima del precio actual de mercado (precio Ask) para una orden Buy Stop
- Por debajo del precio de mercado actual (precio Bid) para una orden Buy Limit
- Por debajo del precio de mercado actual (precio Bid) para una orden Sell Stop
- 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...
- Se abre una posición,
- Una posición se cierra parcial o totalmente,
- 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
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Del nivel básico al intermedio: Subventanas (II)
Red neuronal en la práctica: Cuándo usar una neurona artificial y entender su función en MQL5
Particularidades del trabajo con números del tipo double en MQL4
Del nivel básico al intermedio: Subventanas (I)
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso
