English Русский Deutsch 日本語 Português
preview
Sistema de arbitraje de alta frecuencia en Python con MetaTrader 5

Sistema de arbitraje de alta frecuencia en Python con MetaTrader 5

MetaTrader 5Trading | 4 abril 2025, 14:40
448 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Introducción

Mercado de divisas. Estrategias algorítmicas. Python y MetaTrader 5. Todo esto se juntó cuando empecé a trabajar en un sistema comercial de arbitraje. La idea era sencilla: crear un sistema de alta frecuencia para encontrar desequilibrios de precios. ¿Y qué resultó al final?

Durante este periodo, utilicé la API de MetaTrader 5 con mayor frecuencia. Empecé con esta idea: decidí contar las tasas cruzadas sintéticas, sin limitarme a diez o cien. La cifra llegó a miles.

Por otro lado, estaba el reto de la gestión de riesgos. En este artículo hablaré de todo ello. Arquitectura de sistemas, algoritmos, toma de decisiones: lo desglosaremos todo. Asimismo, veremos los resultados del backtesting y de las operaciones en vivo. Y, obviamente, compartiremos ideas para el futuro. Quién sabe, quizá alguno de ustedes quiera profundizar en este tema... Espero que mi trabajo tenga cierto éxito. Me gustaría creer que contribuirá al desarrollo del trading algorítmico. Quizá alguien lo tome como base y cree algo todavía mejor en el mundo del arbitraje de alta frecuencia. Al fin y al cabo, ese es el principio esencial de la ciencia: avanzar basándose en la experiencia de los predecesores. Vamos directamente al grano.


Introducción al arbitraje de divisas

El arbitraje en Fórex: vamos a desglosar lo que verdaderamente supone.

Se puede establecer una analogía con el cambio de divisas. Digamos que puede comprar dólares por euros en un sitio, venderlos inmediatamente por libras en otro y luego volver a cambiar las libras por euros y acabar en el lado positivo. Esto sería el arbitraje en su forma más simple.

En realidad, sin embargo, resulta un poco más complicado que eso. Fórex es un mercado enorme y descentralizado, con un gran número de bancos, brókeres, fondos... Y cada uno tiene sus propios tipos de cambio. La mayoría de las veces, estos tipos no coinciden, y ahí es donde aparece la oportunidad del arbitraje. Pero no se crea que es dinero fácil: normalmente, estas discrepancias de precios perduran durante segundos, o incluso milisegundos. Es casi imposible seguir el ritmo. Se necesitan ordenadores potentes y algoritmos rápidos.

Además, existen diferentes tipos de arbitraje. El arbitraje simple es cuando jugamos con la diferencia de tipos en distintos lugares. El complicado es cuando utilizamos tipos cruzados. Por ejemplo, calculamos cuánto valdrá la libra a través del dólar y el euro y lo comparamos con el tipo de cambio directo libra/euro.

La lista no acaba aquí: también está el arbitraje temporal. Aquí jugamos con la diferencia de precios en distintos momentos, por ejemplo, comprando ahora, y vendiendo en un minuto. Sí, el proceso parece sencillo, pero el principal problema es que no sabemos adónde irá el precio dentro de un minuto. Ahí es donde residen los mayores riesgos. El mercado puede dar la vuelta antes de podamos activar la orden correcta. O puede que nuestro bróker se retrase en la ejecución de las órdenes. En definitiva, las complejidades y los riesgos son bastante numerosos. A pesar de todas las dificultades, el arbitraje de divisas es un sistema bastante solicitado. Hay recursos monetarios muy sustanciosos en juego y bastantes operadores especializados exclusivamente en esto.

Ahora, tras esta pequeña introducción, vamos a entrar de lleno en nuestro trabajo con las estrategias.


Panorámica de las tecnologías utilizadas: Python y MetaTrader 5

Bien, Python y MetaTrader 5. 

Python tiene muchas funciones y es fácil de entender. No en vano, es el preferido tanto de programadores noveles como de expertos. Y para analizar datos, resulta el más adecuado.

Por otra parte, tenemos MetaTrader 5, una plataforma conocida por todos los tráders de divisas. Es fiable y además no es complicada, eso por no hablar de su enorme funcionalidad, con cotizaciones en tiempo real, robots comerciales y análisis técnico, todo en una sola aplicación. Bien, pues para lograr resultados positivos, tenemos que aunar todo esto.

Qué es lo que sucede: Python toma los datos de MetaTrader 5, los procesa usando sus bibliotecas, y luego envía comandos de nuevo a MetaTrader 5 para ejecutar las operaciones. Por supuesto, existirán ciertas dificultades. Pero juntas, estas aplicaciones resultan muy productivas.

Para trabajar con MetaTrader 5 desde Python, existe una biblioteca especial de los desarrolladores. Para activarla, solo tendremos que instalarla. Así podremos recibir cotizaciones, enviar órdenes y gestionar posiciones. Es como el propio terminal, salvo que ahora también se usan funciones de Python.

¿De qué características y funciones dispondremos? Tendremos bastantes. Por ejemplo, funciones para automatizar la negociación o realizar sofisticados análisis de datos históricos. Incluso podremos crear nuestra propia plataforma comercial. Esto ya es tarea de usuarios avanzados, pero también será posible.


Configuración del entorno: instalación de las bibliotecas necesarias y conexión a MetaTrader 5

Comenzaremos a trabajar con Python. Si aún no lo tiene instalado, visite python.org. Descárguelo e instálelo. También deberá establecer el consentimiento de "ADD TO PATCH".

Nuestro siguiente paso serán las bibliotecas. Vamos a necesitar unas cuantas. La principal será MetaTrader5. La instalación no requiere conocimientos especiales.

Simplemente abra la línea de comandos y escriba:

pip install MetaTrader5 pandas numpy

Pulse Enter y vaya a por un café. O un té. O lo que prefiera.

¿Está todo instalado? Ahora tendremos que realizar la conexión a MetaTrader 5.

Lo primero que deberemos hacer es instalar la propia plataforma MetaTrader 5. Descárguela de su bróker. Lo importante es que recuerde la ruta al terminal. Por lo general, será algo parecido a "C:\ProgramFiles\MetaTrader 5\terminal64.exe". Recuerde esta ruta, la necesitará.

Ahora abriremos Python y escribiremos:

import MetaTrader5 as mt5

if not mt5.initialize(path="C:/Program Files/MetaTrader 5/terminal64.exe"):
    print("Alas! Failed to connect :(")
    mt5.shutdown()
else:
    print("Hooray! Connection successful!")

Si todo se ha puesto en marcha, bien, ahora podremos pasar a la siguiente parte.


La estructura de nuestro código: principales funciones y finalidad de estas

Empezaremos por los imports. Aquí tendremos importaciones como: MetaTrader5, pandas, datetime, pytz... A continuación, vendrán las funciones.

  • La primera función será remove_duplicate_indices. Esta se asegura de que no haya repeticiones en nuestros datos.
  • La siguiente será get_mt5_data. Accede a las funciones de MetaTrader 5 y recupera los datos necesarios, además, en las últimas 24 horas.
  • get_currency_data es una función muy interesante. Llama a get_mt5_data para muchos pares de divisas. AUDUSD, EURUSD, GBPJPY y muchos pares más.
  • La siguiente será calculate_synthetic_prices. Esta función es todo un logro. Trabajando con los pares de divisas, produce cientos de precios sintéticos.
  • analyse_arbitrage - busca oportunidades de arbitraje comparando precios reales con precios sintéticos, y registra todos los hallazgos en un archivo CSV.
  • open_test_limit_order será otra unidad eficiente de nuestro código. Cuando se encuentra una oportunidad de arbitraje, esta función abre una orden de prueba, pero no más de 10 transacciones abiertas al mismo tiempo.

Y por último, la función principal, que gestiona todo este proceso llamando a las funciones en el orden correcto.

Y al final, es un ciclo infinito. Realiza el ciclo completo cada 5 minutos, pero solo en horario laboral. Esa será nuestra estructura. Simple, pero eficaz. 


Obtención de datos de MetaTrader 5: la función get_mt5_data

La primera tarea consistirá en recuperar los datos del terminal.

if not mt5.initialize(path=terminal_path):
    print(f"Failed to connect to MetaTrader 5 terminal at {terminal_path}")
    return None
timezone = pytz.timezone("Etc/UTC")
utc_from = datetime.now(timezone) - timedelta(days=1)

Tenga en cuenta que estamos usando UTC. Porque en el mundo de Fórex no hay lugar para la confusión con los husos horarios.

Ahora lo más importante es conseguir los ticks:

ticks = mt5.copy_ticks_from(symbol, utc_from, count, mt5.COPY_TICKS_ALL)

¿Ha conseguido los datos? Excelente. Ahora tendremos que procesarlos. Para ello utilizaremos pandas:

ticks_frame = pd.DataFrame(ticks)
ticks_frame['time'] = pd.to_datetime(ticks_frame['time'], unit='s')

¡Voilà! Ahora tenemos nuestro propio DataFrame con datos. Ya está preparado para su análisis.

Pero, ¿y si algo sale mal? No se preocupe, nuestra función también lo ha previsto:

if ticks is None:
    print(f"Failed to fetch data for {symbol}")
    return None

Simplemente informará del problema y retornará None. Así será nuestra función get_mt5_data. 


Trabajamos con varios pares de divisas: función get_currency_data

Ahora nos sumergiremos más en el sistema con la función get_currency_data. Echemos un vistazo al código:

def get_currency_data():
    # Define currency pairs and the amount of data
    symbols = ["AUDUSD", "AUDJPY", "CADJPY", "AUDCHF", "AUDNZD", "USDCAD", "USDCHF", "USDJPY", "NZDUSD", "GBPUSD", "EURUSD", "CADCHF", "CHFJPY", "NZDCAD", "NZDCHF", "NZDJPY", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD"]
    count = 1000  # number of data points for each currency pair
    data = {}
    for symbol in symbols:
        df = get_mt5_data(symbol, count, terminal_path)
        if df is not None:
            data[symbol] = df[['time', 'bid', 'ask']].set_index('time')
    return data

Empieza con la definición de los pares de divisas. La lista incluye AUDUSD, EURUSD, GBPJPY y otros instrumentos populares.

Entonces pasaremos al siguiente paso. La función creará un diccionario de datos vacío. Posteriormente, también se rellenará con los datos necesarios.

Ahora nuestra función iniciará su trabajo. Recorrerá una lista de pares de divisas, y para cada par, llamará a get_mt5_data. Si get_mt5_data retorna datos (y no None), nuestra función solo tomará lo esencial: la hora, Bid y Ask.

Y ahora, por fin, el gran final. La función retornará un diccionario lleno de datos. 

Ahora obtendremos getting_currency_data. Una función pequeña, potente, sencilla pero eficaz.


Cálculo de los precios sintéticos de 2000: estrategia y aplicación

Ahora nos sumergiremos en los fundamentos de nuestro sistema: la función calculate_synthetic_prices. Gracias a ella, obtendremos nuestros datos sintetizados.

Echemos un vistazo a este código:

def calculate_synthetic_prices(data):
    synthetic_prices = {}

    # Remove duplicate indices from all DataFrames in the data dictionary
    for key in data:
        data[key] = remove_duplicate_indices(data[key])

    # Calculate synthetic prices for all pairs using multiple methods
    pairs = [('AUDUSD', 'USDCHF'), ('AUDUSD', 'NZDUSD'), ('AUDUSD', 'USDJPY'),
             ('USDCHF', 'USDCAD'), ('USDCHF', 'NZDCHF'), ('USDCHF', 'CHFJPY'),
             ('USDJPY', 'USDCAD'), ('USDJPY', 'NZDJPY'), ('USDJPY', 'GBPJPY'),
             ('NZDUSD', 'NZDCAD'), ('NZDUSD', 'NZDCHF'), ('NZDUSD', 'NZDJPY'),
             ('GBPUSD', 'GBPCAD'), ('GBPUSD', 'GBPCHF'), ('GBPUSD', 'GBPJPY'),
             ('EURUSD', 'EURCAD'), ('EURUSD', 'EURCHF'), ('EURUSD', 'EURJPY'),
             ('CADCHF', 'CADJPY'), ('CADCHF', 'GBPCAD'), ('CADCHF', 'EURCAD'),
             ('CHFJPY', 'GBPCHF'), ('CHFJPY', 'EURCHF'), ('CHFJPY', 'NZDCHF'),
             ('NZDCAD', 'NZDJPY'), ('NZDCAD', 'GBPNZD'), ('NZDCAD', 'EURNZD'),
             ('NZDCHF', 'NZDJPY'), ('NZDCHF', 'GBPNZD'), ('NZDCHF', 'EURNZD'),
             ('NZDJPY', 'GBPNZD'), ('NZDJPY', 'EURNZD')]

    method_count = 1
    for pair1, pair2 in pairs:
        print(f"Calculating synthetic price for {pair1} and {pair2} using method {method_count}")
        synthetic_prices[f'{pair1}_{method_count}'] = data[pair1]['bid'] / data[pair2]['ask']
        method_count += 1
        print(f"Calculating synthetic price for {pair1} and {pair2} using method {method_count}")
        synthetic_prices[f'{pair1}_{method_count}'] = data[pair1]['bid'] / data[pair2]['bid']
        method_count += 1

    return pd.DataFrame(synthetic_prices)


Análisis de las oportunidades del arbitraje: la función analyze_arbitrage

¿Y ahora qué? En primer lugar, crearemos un diccionario synthetic_prices vacío. También lo rellenaremos con datos. A continuación, revisaremos todos los datos y eliminaremos los índices duplicados para evitar errores posteriores.

Ahora el siguiente paso consistirá hacer una lista de pares. Estos serán los pares de divisas que utilizaremos para la síntesis. A continuación, comenzará otro proceso. Luego iniciaremos un ciclo por todas las parejas. Para cada par, calcularemos el precio sintético de dos maneras:

  1. Dividiremos el primer par por el segundo par.
  2. Dividiremos el primer par por el Bid del segundo par.

Y cada vez aumentaremos nuestro method_count. Acabaremos teniendo no 1.000, ni 1.500, ¡sino hasta 2.000 precios sintéticos!

Así funciona la función calculate_synthetic_prices. No solo calcula los precios, sino que esencialmente crea nuevas oportunidades. ¡Esta característica ofrece grandes resultado en forma de oportunidades de arbitraje!


Visualización de resultados: guardamos los datos en CSV

Vamos a analizar la función analyse_arbitrage. Esta no se limita a analizar los datos, sino que busca lo que hace falta en el flujo de números. Echémosle un vistazo:

def analyze_arbitrage(data, synthetic_prices, method_count):
    # Calculate spreads for each pair
    spreads = {}
    for pair in data.keys():
        for i in range(1, method_count + 1):
            synthetic_pair = f'{pair}_{i}'
            if synthetic_pair in synthetic_prices.columns:
                print(f"Analyzing arbitrage opportunity for {synthetic_pair}")
                spreads[synthetic_pair] = data[pair]['bid'] - synthetic_prices[synthetic_pair]
    # Identify arbitrage opportunities
    arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008
    print("Arbitrage opportunities:")
    print(arbitrage_opportunities)
    # Save the full table of arbitrage opportunities to a CSV file
    arbitrage_opportunities.to_csv('arbitrage_opportunities.csv')
    return arbitrage_opportunities

En primer lugar, nuestra función creará un diccionario spreads vacío. También lo rellenaremos con datos.

Procedemos al siguiente paso. La función se ejecuta con todos los pares de divisas y sus homólogos sintéticos. Para cada par, se calculará el spread, es decir, la diferencia entre el precio de compra real y el precio sintético.

spreads[synthetic_pair] = data[pair]['bid'] - synthetic_prices[synthetic_pair]

Esta línea desempeñará un papel muy importante, pues encontrará la diferencia entre el precio real y el sintético. Si esa diferencia es positiva, tendremos una oportunidad de arbitraje.

Para lograr resultados más serios, utilizaremos el número 0,00008:

arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008

Esta línea descartará todas las oportunidades de menos de 8 puntos. De esta forma, obtendremos oportunidades con más probabilidades de rentabilidad.

Y aquí tenemos el siguiente paso:

arbitrage_opportunities.to_csv('arbitrage_opportunities.csv')

Ahora todos nuestros datos estarán guardados en un archivo CSV. Ahora podremos estudiarlos, analizarlos, construir gráficos... en general, hacer un trabajo productivo. Todo gracias a la siguiente función: analyse_arbitrage. Esta no se limita a analizar, sino que busca, encuentra y guarda las oportunidades de arbitraje.


Apertura de órdenes de prueba: la función open_test_limit_order

A continuación, analizaremos la siguiente función open_test_limit_order. Esta abrirá las órdenes para nosotros.

Echémosle un vistazo:

def open_test_limit_order(symbol, order_type, price, volume, take_profit, stop_loss, terminal_path):
    if not mt5.initialize(path=terminal_path):
        print(f"Failed to connect to MetaTrader 5 terminal at {terminal_path}")
        return None
    symbol_info = mt5.symbol_info(symbol)
    positions_total = mt5.positions_total()
    if symbol_info is None:
        print(f"Instrument not found: {symbol}")
        return None
    if positions_total >= MAX_OPEN_TRADES:
        print("MAX POSITIONS TOTAL!")
        return None
    # Check if symbol_info is None before accessing its attributes
    if symbol_info is not None:
        request = {
            "action": mt5.TRADE_ACTION_DEAL,
            "symbol": symbol,
            "volume": volume,
            "type": order_type,
            "price": price,
            "deviation": 30,
            "magic": 123456,
            "comment": "Stochastic Stupi Sustem",
            "type_time": mt5.ORDER_TIME_GTC,
            "type_filling": mt5.ORDER_FILLING_IOC,
            "tp": price + take_profit * symbol_info.point if order_type == mt5.ORDER_TYPE_BUY else price - take_profit * symbol_info.point,
            "sl": price - stop_loss * symbol_info.point if order_type == mt5.ORDER_TYPE_BUY else price + stop_loss * symbol_info.point,
        }
        result = mt5.order_send(request)
        if result is not None and result.retcode == mt5.TRADE_RETCODE_DONE:
            print(f"Test limit order placed for {symbol}")
            return result.order
        else:
            print(f"Error: Test limit order not placed for {symbol}, retcode={result.retcode if result is not None else 'None'}")
            return None
    else:
        print(f"Error: Symbol info not found for {symbol}")
        return None

Lo primero que intentará hacer nuestra función es conectarse al terminal MetaTrader 5. Luego comprobará si el instrumento con el que queremos negociar existe.

El siguiente código:

if positions_total >= MAX_OPEN_TRADES:
    print("MAX POSITIONS TOTAL!")
    return None

Esta comprobación se asegurará de que no abrimos demasiadas posiciones.

Ahora el siguiente paso consistirá en generar una solicitud para abrir una orden. Aquí tenemos bastantes parámetros. Tipo de orden, volumen, precio, desviación, número mágico, comentarios... Si todo se ha ejecutado bien, la función nos lo indicará. Si no, aparecerá un mensaje.

Así trabaja la función open_test_limit_order. Será nuestro enlace con el mercado, y actuará como una especie de bróker.


Restricciones horarias al comercio: trabajamos durante determinadas horas

Ahora hablaremos del tiempo de negociación. 

if current_time >= datetime.strptime("23:30", "%H:%M").time() or current_time <= datetime.strptime("05:00", "%H:%M").time():
    print("Current time is between 23:30 and 05:00. Skipping execution.")
    time.sleep(300)  # Wait for 5 minutes before checking again
    continue

¿Qué ocurre aquí? Nuestro sistema comprueba la hora. Si el reloj marca entre las 23:30 y las 5:00, verá que no estamos en horario comercial y pasará al modo de espera durante 5 minutos. Entonces se activará, comprobará de nuevo la hora y, si aún es pronto, pasará de nuevo al modo de espera.

¿Por qué hace falta esto? Existen razones para ello. En primer lugar, la liquidez, que suele ser menor por la noche. En segundo lugar, los spreads, que se expanden por la noche. En tercer lugar, las noticias. Las más importantes suelen salir en horario laboral.


Ciclo de ejecución y procesamiento de errores

Vamos a analizar la función main. Es como el capitán de un barco, solo que en lugar de un timón, usa un teclado. ¿Qué hace? Es sencillo:

  1. Recopila los datos
  2. Calcula los precios sintéticos 
  3. Busca oportunidades de arbitraje 
  4. Abrir órdenes

También se da un pequeño procesamiento de errores. 

def main():
    data = get_currency_data()
    synthetic_prices = calculate_synthetic_prices(data)
    method_count = 2000  # Define the method_count variable here
    arbitrage_opportunities = analyze_arbitrage(data, synthetic_prices, method_count)

    # Trade based on arbitrage opportunities
    for symbol in arbitrage_opportunities.columns:
        if arbitrage_opportunities[symbol].any():
            direction = "BUY" if arbitrage_opportunities[symbol].iloc[0] else "SELL"
            symbol = symbol.split('_')[0]  # Remove the index from the symbol
            symbol_info = mt5.symbol_info_tick(symbol)
            if symbol_info is not None:
                price = symbol_info.bid if direction == "BUY" else symbol_info.ask
                take_profit = 450
                stop_loss = 200
                order = open_test_limit_order(symbol, mt5.ORDER_TYPE_BUY if direction == "BUY" else mt5.ORDER_TYPE_SELL, price, 0.50, take_profit, stop_loss, terminal_path)
            else:
                print(f"Error: Symbol info tick not found for {symbol}")


Escalabilidad del sistema: incorporación de nuevos pares de divisas y métodos

¿Quiere añadir un nuevo par de divisas? Póngalo en esta lista:

symbols = ["EURUSD", "GBPUSD", "USDJPY", ... , "YOURPAIR"]

El sistema ya conoce el nuevo par. ¿Y los nuevos métodos de cálculo? 

def calculate_synthetic_prices(data):
    # ... существующий код ...
    
    # Добавляем новый метод
    synthetic_prices[f'{pair1}_{method_count}'] = data[pair1]['ask'] / data[pair2]['bid']
    method_count += 1


Pruebas y backtesting del sistema de arbitraje

Hablemos ahora del backtesting. Este es un punto realmente importante para cualquier sistema comercial. Y nuestro sistema de arbitraje no es una excepción.

¿Qué hemos hecho? Hemos aplicado nuestra estrategia a los datos históricos. ¿Para qué? Solo para ver su eficacia. Nuestro código comenzará con get_historical_data. Esta función recuperará datos antiguos de MetaTrader 5. Sin estos datos, desgraciadamente no podremos ser productivos.

Luego vendrá calculate_synthetic_prices. Aquí es donde consideraremos los tipos de cambio sintéticos. Esta es una parte clave de nuestra estrategia de arbitraje. Analyze_arbitrage será nuestro detector de oportunidades. Comparará los precios reales con los precios sintéticos. Además encontrará la diferencia, y podremos obtener un beneficio potencial, mientras que simulate_trade supondrá prácticamente el proceso de negociación. Sin embargo, se mantendrá en el modo de prueba. Es un proceso muy importante: es mejor equivocarse en una simulación que perder dinero real.

Por último, backtest_arbitrage_system pondrá todo junto y ejecutará nuestra estrategia a través de los datos históricos. Día tras día, transacción tras transacción.

import MetaTrader5 as mt5
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
import pytz

# Path to MetaTrader 5 terminal
terminal_path = "C:/Program Files/ForexBroker - MetaTrader 5/Arima/terminal64.exe"

def remove_duplicate_indices(df):
    """Removes duplicate indices, keeping only the first row with a unique index."""
    return df[~df.index.duplicated(keep='first')]

def get_historical_data(start_date, end_date, terminal_path):
    if not mt5.initialize(path=terminal_path):
        print(f"Failed to connect to MetaTrader 5 terminal at {terminal_path}")
        return None

    symbols = ["AUDUSD", "AUDJPY", "CADJPY", "AUDCHF", "AUDNZD", "USDCAD", "USDCHF", "USDJPY", "NZDUSD", "GBPUSD", "EURUSD", "CADCHF", "CHFJPY", "NZDCAD", "NZDCHF", "NZDJPY", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD"]
    
    historical_data = {}
    for symbol in symbols:
        timeframe = mt5.TIMEFRAME_M1
        rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date)
        if rates is not None and len(rates) > 0:
            df = pd.DataFrame(rates)
            df['time'] = pd.to_datetime(df['time'], unit='s')
            df.set_index('time', inplace=True)
            df = df[['open', 'high', 'low', 'close']]
            df['bid'] = df['close']  # Simplification: use 'close' as 'bid'
            df['ask'] = df['close'] + 0.000001  # Simplification: add spread
            historical_data[symbol] = df

    mt5.shutdown()
    return historical_data

def calculate_synthetic_prices(data):
    synthetic_prices = {}
    pairs = [('AUDUSD', 'USDCHF'), ('AUDUSD', 'NZDUSD'), ('AUDUSD', 'USDJPY'),
             ('USDCHF', 'USDCAD'), ('USDCHF', 'NZDCHF'), ('USDCHF', 'CHFJPY'),
             ('USDJPY', 'USDCAD'), ('USDJPY', 'NZDJPY'), ('USDJPY', 'GBPJPY'),
             ('NZDUSD', 'NZDCAD'), ('NZDUSD', 'NZDCHF'), ('NZDUSD', 'NZDJPY'),
             ('GBPUSD', 'GBPCAD'), ('GBPUSD', 'GBPCHF'), ('GBPUSD', 'GBPJPY'),
             ('EURUSD', 'EURCAD'), ('EURUSD', 'EURCHF'), ('EURUSD', 'EURJPY'),
             ('CADCHF', 'CADJPY'), ('CADCHF', 'GBPCAD'), ('CADCHF', 'EURCAD'),
             ('CHFJPY', 'GBPCHF'), ('CHFJPY', 'EURCHF'), ('CHFJPY', 'NZDCHF'),
             ('NZDCAD', 'NZDJPY'), ('NZDCAD', 'GBPNZD'), ('NZDCAD', 'EURNZD'),
             ('NZDCHF', 'NZDJPY'), ('NZDCHF', 'GBPNZD'), ('NZDCHF', 'EURNZD'),
             ('NZDJPY', 'GBPNZD'), ('NZDJPY', 'EURNZD')]

    for pair1, pair2 in pairs:
        if pair1 in data and pair2 in data:
            synthetic_prices[f'{pair1}_{pair2}_1'] = data[pair1]['bid'] / data[pair2]['ask']
            synthetic_prices[f'{pair1}_{pair2}_2'] = data[pair1]['bid'] / data[pair2]['bid']

    return pd.DataFrame(synthetic_prices)

def analyze_arbitrage(data, synthetic_prices):
    spreads = {}
    for pair in data.keys():
        for synth_pair in synthetic_prices.columns:
            if pair in synth_pair:
                spreads[synth_pair] = data[pair]['bid'] - synthetic_prices[synth_pair]

    arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008
    return arbitrage_opportunities

def simulate_trade(data, direction, entry_price, take_profit, stop_loss):
    for i, row in data.iterrows():
        current_price = row['bid'] if direction == "BUY" else row['ask']
        
        if direction == "BUY":
            if current_price >= entry_price + take_profit:
                return {'profit': take_profit * 800, 'duration': i}
            elif current_price <= entry_price - stop_loss:
                return {'profit': -stop_loss * 400, 'duration': i}
        else:  # SELL
            if current_price <= entry_price - take_profit:
                return {'profit': take_profit * 800, 'duration': i}
            elif current_price >= entry_price + stop_loss:
                return {'profit': -stop_loss * 400, 'duration': i}
    
    # If the loop completes without hitting TP or SL, close at the last price
    last_price = data['bid'].iloc[-1] if direction == "BUY" else data['ask'].iloc[-1]
    profit = (last_price - entry_price) * 100000 if direction == "BUY" else (entry_price - last_price) * 100000
    return {'profit': profit, 'duration': len(data)}

def backtest_arbitrage_system(historical_data, start_date, end_date):
    equity_curve = [10000]  # Starting with $10,000
    trades = []
    dates = pd.date_range(start=start_date, end=end_date, freq='D')

    for current_date in dates:
        print(f"Backtesting for date: {current_date.date()}")
        
        # Get data for the current day
        data = {symbol: df[df.index.date == current_date.date()] for symbol, df in historical_data.items()}
        
        # Skip if no data for the current day
        if all(df.empty for df in data.values()):
            continue

        synthetic_prices = calculate_synthetic_prices(data)
        arbitrage_opportunities = analyze_arbitrage(data, synthetic_prices)

        # Simulate trades based on arbitrage opportunities
        for symbol in arbitrage_opportunities.columns:
            if arbitrage_opportunities[symbol].any():
                direction = "BUY" if arbitrage_opportunities[symbol].iloc[0] else "SELL"
                base_symbol = symbol.split('_')[0]
                if base_symbol in data and not data[base_symbol].empty:
                    price = data[base_symbol]['bid'].iloc[-1] if direction == "BUY" else data[base_symbol]['ask'].iloc[-1]
                    take_profit = 800 * 0.00001  # Convert to price
                    stop_loss = 400 * 0.00001  # Convert to price
                    
                    # Simulate trade
                    trade_result = simulate_trade(data[base_symbol], direction, price, take_profit, stop_loss)
                    trades.append(trade_result)
                    
                    # Update equity curve
                    equity_curve.append(equity_curve[-1] + trade_result['profit'])

    return equity_curve, trades

def main():
    start_date = datetime(2024, 1, 1, tzinfo=pytz.UTC)
    end_date = datetime(2024, 8, 31, tzinfo=pytz.UTC)  # Backtest for January-August 2024
    
    print("Fetching historical data...")
    historical_data = get_historical_data(start_date, end_date, terminal_path)
    
    if historical_data is None:
        print("Failed to fetch historical data. Exiting.")
        return

    print("Starting backtest...")
    equity_curve, trades = backtest_arbitrage_system(historical_data, start_date, end_date)

    total_profit = sum(trade['profit'] for trade in trades)
    win_rate = sum(1 for trade in trades if trade['profit'] > 0) / len(trades) if trades else 0

    print(f"Backtest completed. Results:")
    print(f"Total Profit: ${total_profit:.2f}")
    print(f"Win Rate: {win_rate:.2%}")
    print(f"Final Equity: ${equity_curve[-1]:.2f}")

    # Plot equity curve
    plt.figure(figsize=(15, 10))
    plt.plot(equity_curve)
    plt.title('Equity Curve: Backtest Results')
    plt.xlabel('Trade Number')
    plt.ylabel('Account Balance ($)')
    plt.savefig('equity_curve.png')
    plt.close()

    print("Equity curve saved as 'equity_curve.png'.")

if __name__ == "__main__":
    main()

¿Por qué esto es importante? Porque las pruebas retrospectivas demuestran la eficacia de nuestro sistema. ¿Es rentable o agota el depósito? ¿Qué reducción muestra? ¿Cuál es el porcentaje de transacciones ganadoras? Es del backtest de donde aprenderemos todo esto.

Por supuesto, los resultados pasados no garantizan buenos resultados futuros. El mercado cambia. Pero sin backtests, no obtendremos ningún resultado. Y con ellos, sabremos más o menos qué esperar. Y otro punto importante: el backtesting ayuda a optimizar el sistema. Cambiamos los parámetros, miramos los resultados. Cambiamos de nuevo, miramos de nuevo. Así que, paso a paso, vamos mejorando nuestro sistema.

Bien, aquí tenemos el resultado de nuestro backtest del sistema:

Aquí tenemos la prueba del sistema en MetaTrader 5:

Aquí tenemos el código del asesor experto MQL5 para este sistema:

//+------------------------------------------------------------------+
//|                                                 TrissBotDemo.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
// Input parameters
input int MAX_OPEN_TRADES = 10;
input double VOLUME = 0.50;
input int TAKE_PROFIT = 450;
input int STOP_LOSS = 200;
input double MIN_SPREAD = 0.00008;

// Global variables
string symbols[] = {"AUDUSD", "AUDJPY", "CADJPY", "AUDCHF", "AUDNZD", "USDCAD", "USDCHF", "USDJPY", "NZDUSD", "GBPUSD", "EURUSD", "CADCHF", "CHFJPY", "NZDCAD", "NZDCHF", "NZDJPY", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD"};
int symbolsTotal;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    symbolsTotal = ArraySize(symbols);
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    // Cleanup code here
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    if(!IsTradeAllowed()) return;
    
    datetime currentTime = TimeGMT();
    if(currentTime >= StringToTime("23:30:00") || currentTime <= StringToTime("05:00:00"))
    {
        Print("Current time is between 23:30 and 05:00. Skipping execution.");
        return;
    }
    
    AnalyzeAndTrade();
}

//+------------------------------------------------------------------+
//| Analyze arbitrage opportunities and trade                        |
//+------------------------------------------------------------------+
void AnalyzeAndTrade()
{
    double synthetic_prices[];
    ArrayResize(synthetic_prices, symbolsTotal);
    
    for(int i = 0; i < symbolsTotal; i++)
    {
        synthetic_prices[i] = CalculateSyntheticPrice(symbols[i]);
        double currentPrice = SymbolInfoDouble(symbols[i], SYMBOL_BID);
        
        if(MathAbs(currentPrice - synthetic_prices[i]) > MIN_SPREAD)
        {
            if(currentPrice > synthetic_prices[i])
            {
                OpenOrder(symbols[i], ORDER_TYPE_SELL);
            }
            else
            {
                OpenOrder(symbols[i], ORDER_TYPE_BUY);
            }
        }
        
    }
}

//+------------------------------------------------------------------+
//| Calculate synthetic price for a symbol                           |
//+------------------------------------------------------------------+
double CalculateSyntheticPrice(string symbol)
{
    // This is a simplified version. You need to implement the logic
    // to calculate synthetic prices based on your specific method
    return SymbolInfoDouble(symbol, SYMBOL_ASK);
}

//+------------------------------------------------------------------+
//| Open a new order                                                 |
//+------------------------------------------------------------------+
void OpenOrder(string symbol, ENUM_ORDER_TYPE orderType)
{
    if(PositionsTotal() >= MAX_OPEN_TRADES)
    {
        Print("MAX POSITIONS TOTAL!");
        return;
    }
    
    double price = (orderType == ORDER_TYPE_BUY) ? SymbolInfoDouble(symbol, SYMBOL_ASK) : SymbolInfoDouble(symbol, SYMBOL_BID);
    double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
    
    double tp = (orderType == ORDER_TYPE_BUY) ? price + TAKE_PROFIT * point : price - TAKE_PROFIT * point;
    double sl = (orderType == ORDER_TYPE_BUY) ? price - STOP_LOSS * point : price + STOP_LOSS * point;
    
    MqlTradeRequest request = {};
    MqlTradeResult result = {};
    
    request.action = TRADE_ACTION_DEAL;
    request.symbol = symbol;
    request.volume = VOLUME;
    request.type = orderType;
    request.price = price;
    request.deviation = 30;
    request.magic = 123456;
    request.comment = "ArbitrageAdvisor";
    request.type_time = ORDER_TIME_GTC;
    request.type_filling = ORDER_FILLING_IOC;
    request.tp = tp;
    request.sl = sl;
    
    if(!OrderSend(request, result))
    {
        Print("OrderSend error ", GetLastError());
        return;
    }
    
    if(result.retcode == TRADE_RETCODE_DONE)
    {
        Print("Order placed successfully");
    }
    else
    {
        Print("Order failed with retcode ", result.retcode);
    }
}

//+------------------------------------------------------------------+
//| Check if trading is allowed                                      |
//+------------------------------------------------------------------+
bool IsTradeAllowed()
{
    if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
    {
        Print("Trade is not allowed in the terminal");
        return false;
    }
    
    if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
    {
        Print("Trade is not allowed in the Expert Advisor");
        return false;
    }
    
    return true;
}


Formas de mejorar y legalizar la estrategia para los brókeres, o cómo no poner en peligro al proveedor de liquidez utilizando órdenes limitadas.

No obstante, nuestro sistema presenta otros retos. Los brókeres y proveedores de liquidez suelen ver con malos ojos estos sistemas. ¿Por qué? Pues porque esencialmente estamos retirando la liquidez necesaria del mercado. Estos han acuñado incluso un término especial para ello: "Toxic Order Flow". 

Es un problema real. Con nuestras órdenes de mercado, estamos literalmente comiendo liquidez del sistema. Y todo el mundo lo necesita: tanto los grandes tráders como los pequeños. Obviamente, esto tiene sus consecuencias.

¿Qué debemos hacer en esta situación? Existe una solución intermedia: las órdenes limitadas. 

Pero esto no resuelve todos los problemas: la etiqueta "Toxic Order Flow" se pone no tanto por la absorción de liquidez del mercado en el momento, sino por las elevadas cargas que supone dar servicio a tal flujo de órdenes. Es un problema que aún no se ha resuelto. Por ejemplo, gastar 100 dólares (tomemos la cifra teóricamente) para dar servicio a un enorme flujo de operaciones del arbitrajista, recibiendo de él una comisión digamos de 50, no es algo rentable. Así que quizás la clave aquí sea la alta circulación y el gran tamaño de los lotes, así como las altas tasas de circulación. Entonces, ¿los brókeres también estarán dispuestos a pagar descuentos?

Ahora hablemos del código. ¿Cómo podemos mejorarlo? En primer lugar, podemos añadir una función para gestionar órdenes limitadas. Aquí también hay mucho trabajo: debemos pensar en la lógica de la espera y la cancelación de las órdenes no ejecutadas.

Una idea interesante para mejorar el sistema sería el aprendizaje automático. Supongo que podemos enseñar a nuestro sistema a predecir qué oportunidades de arbitraje tienen más probabilidades de funcionar. 


Conclusión

Resumamos. Hoy hemos creado un sistema que busca oportunidades de arbitraje. Recuerde que nuestro sistema no supone la solución a todos sus problemas financieros. 

Asimismo, nos hemos ocupado del backtesting. Este trabaja con datos temporales y, lo que es aún mejor, nos permite ver cómo habría funcionado nuestro sistema en el pasado. Pero no lo olvide: los resultados pasados no garantizan beneficios futuros. El mercado es un mecanismo complejo y está en cambio constante.

¿Pero sabe qué es lo más importante? No es el código, ni los algoritmos, sino usted mismo. Su voluntad de aprender, experimentar, equivocarse y volver a intentarlo. Eso sí que no tiene precio.

Así que no se detenga. Este sistema es solo el comienzo de su viaje en el mundo del trading algorítmico. Úselo como punto de partida para nuevas ideas, nuevas estrategias. Y recuerde: en el comercio, como en la vida, lo principal es el equilibrio: entre el riesgo y la prudencia, entre la codicia y lo razonable, entre la complejidad y la sencillez.

Buena suerte en este apasionante viaje, y que sus algoritmos vayan siempre un paso por delante del mercado.

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/15964

Características del Wizard MQL5 que debe conocer (Parte 36): Q-Learning con Cadenas de Markov Características del Wizard MQL5 que debe conocer (Parte 36): Q-Learning con Cadenas de Markov
El aprendizaje de refuerzo es uno de los tres principios principales del aprendizaje automático, junto con el aprendizaje supervisado y el aprendizaje no supervisado. Por lo tanto, se preocupa del control óptimo o de aprender la mejor política a largo plazo que se adapte mejor a la función objetivo. Con este telón de fondo, exploramos su posible papel en la información del proceso de aprendizaje de una MLP de un Asesor Experto montado por un asistente.
Algoritmo de Irrigación Artificial — Artificial Showering Algorithm (ASHA) Algoritmo de Irrigación Artificial — Artificial Showering Algorithm (ASHA)
Este artículo presenta el Algoritmo de Irrigación Artificial (ASHA), un nuevo método metaheurístico desarrollado para resolver problemas generales de optimización. Basado en la modelización de los procesos de flujo y almacenamiento del agua, este algoritmo construye el concepto de un campo ideal en el que cada unidad de recurso (agua) es invocada para encontrar una solución óptima. Hoy descubriremos cómo el ASHA adapta los principios de flujo y acumulación para asignar eficazmente los recursos en el espacio de búsqueda, y también veremos su aplicación y los resultados de sus pruebas.
Búsqueda de patrones arbitrarios de pares de divisas en Python con ayuda de MetaTrader 5 Búsqueda de patrones arbitrarios de pares de divisas en Python con ayuda de MetaTrader 5
¿Existen patrones y regularidades recurrentes en el mercado de divisas? He decidido crear mi propio sistema de análisis de patrones usando Python y MetaTrader 5. Una simbiosis de matemáticas y programación para conquistar Fórex.
Métodos de William Gann (Parte III): ¿Funciona la astrología? Métodos de William Gann (Parte III): ¿Funciona la astrología?
¿Las posiciones de los planetas y las estrellas afectan los mercados financieros? Armémonos de estadísticas y big data y embarquémonos en un viaje apasionante hacia el mundo donde las estrellas y los gráficos bursátiles se cruzan.