Русский Português
preview
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

MetaTrader 5Trading | 7 abril 2025, 10:07
284 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Introducción al Análisis de Patrones Fórex

¿Qué ve un principiante cuando al mirar por primera vez los gráficos de pares de divisas? Muchas oscilaciones intradía, volatilidad al alza y a la baja, cambios de tendencia y mucho más cosas. Subidas, bajadas, zigzags... ¿cómo entenderlo todo? En mi día, yo también comencé a familiarizarme con Fórex y a sumergirme en el análisis de patrones de precios.

Muchas cosas en nuestro mundo parecen caóticas a primera vista, pero cualquier profesional con experiencia en un campo que a los demás les parece confuso ve patrones y oportunidades. Lo mismo sucede con los gráficos de pares de divisas. Si intentamos sistematizar este caos, podremos descubrir patrones ocultos que sugieren futuros movimientos en los precios.

Pero, ¿cómo encontrar dichos patrones? ¿Cómo distinguir un patrón real del ruido aleatorio? Ahí es donde entra la parte más divertida. 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.

La idea era la siguiente: examinar un montón de datos históricos con un algoritmo que localizase patrones recurrentes y evaluara su rendimiento. ¿Le parece interesante? Pues en realidad, la aplicación resultó no ser tan sencilla.


Configuramos el entorno: instalación de las bibliotecas necesarias y conexión a MetaTrader 5

Bien, nuestra primera tarea será instalar Python. Podrá descargarlo desde el sitio web oficial python.org. Descárguelo e instálelo. También deberá marcar la casilla "Add Python a PATH". 

El siguiente paso importante serán las bibliotecas. Vamos a necesitar unas cuantas. Inicio - MetaTrader 5. También pandas resulta útil para trabajar con datos. Y quizás numpy. Abra un símbolo del sistema y escriba:

pip install MetaTrader5 pandas numpy matplotlib pytz

Lo primero que deberá hacer es instalar el propio MetaTrader 5. Descárguelo desde el sitio oficial, el sitio de su bróker, e instálelo. No es complicado en absoluto.

Ahora tendremos que encontrar la ruta al terminal. Normalmente es algo como "C:\Program Files\MetaTrader 5\terminal64.exe". Recuerde esta ruta, la necesitaremos.

Ahora abriremos Python y escribiremos:

import MetaTrader5 as mt5

if not mt5.initialize(path="C:/Program Files/MetaTrader 5/terminal64.exe"):
    print("MetaTrader 5 initialization failed.")
    mt5.shutdown()
else:
    print("MetaTrader 5 initialized successfully.")

Luego lo iniciaremos y si vemos una impresión informando de que el terminal se ha inicializado con éxito, significará que hemos hecho todo correctamente.

¿Quiere asegurarse de que todo funciona? Intentemos obtener algunos datos:

import MetaTrader5 as mt5
import pandas as pd
from datetime import datetime

if not mt5.initialize():
    print("Oops! Something went wrong.")
    mt5.shutdown()

eurusd_ticks = mt5.copy_ticks_from("EURUSD", datetime.now(), 10, mt5.COPY_TICKS_ALL)
ticks_frame = pd.DataFrame(eurusd_ticks)
print("Look at this beauty:")
print(ticks_frame)

mt5.shutdown()

Si ve una tabla de datos, ¡enhorabuena! Acaba de dar su primer paso en el mundo del trading algorítmico de divisas utilizando Python. No es tan difícil como parece.


Estructura del código: principales funciones y objetivos de estas

Vamos a analizar la estructura del código. Se trata de un sistema completo para analizar patrones en el mercado de divisas. 

Empezaremos por lo básico del sistema: la función find_patterns. Esta función analiza los datos históricos, identificando patrones de una longitud determinada. Una vez encontrados los primeros patrones, se evaluará su eficacia. Esta función también recuerda el último patrón, para su uso futuro.

La siguiente función será calculate_winrate_and_frequency. Esta función analiza los patrones encontrados, incluida la frecuencia de aparición y la tasa de victorias, así como la clasificación de los patrones.

La función process_currency_pair también juega un papel importante. Es un manejador bastante importante. Descarga los datos, los analiza, busca patrones de distintas longitudes y nos ofrece los 300 mejores patrones de venta y compra. En cuanto al principio del código, aquí tenemos la inicialización, la configuración de parámetros, el intervalo del gráfico (TF) y el periodo temporal (en mi caso, de 1990 a 2024).

Ahora pasaremos al ciclo de ejecución del código principal. Entre las peculiaridades del algoritmo de búsqueda de patrones podemos mencionar la diferente longitud de los patrones: los cortos son frecuentes pero no aportan fiabilidad, mientras que los largos son demasiado raros, aunque son más eficientes. Deberemos considerar todas las dimensionalidades.


Obtención de datos de MetaTrader 5: función copy_rates_range

Nuestra primera función será la obtención de datos del terminal. Echemos un vistazo al código:

import MetaTrader5 as mt5
import pandas as pd
import time
from datetime import datetime, timedelta
import pytz

# List of major currency pairs
major_pairs = ['EURUSD']

# Setting up data request parameters
timeframe = mt5.TIMEFRAME_H4
start_date = pd.Timestamp('1990-01-01')
end_date = pd.Timestamp('2024-05-31')

def process_currency_pair(symbol):
    max_retries = 5
    retries = 0
    while retries < max_retries:
        try:
            # Loading OHLC data
            rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date)
            if rates is None:
                raise ValueError("No data received")
            ohlc_data = pd.DataFrame(rates)
            ohlc_data['time'] = pd.to_datetime(ohlc_data['time'], unit='s')
            break
        except Exception as e:
            print(f"Error loading data for {symbol}: {e}")
            retries += 1
            time.sleep(2)  # Wait before retrying

    if retries == max_retries:
        print(f"Failed to load data for {symbol} after {max_retries} attempts")
        return

    # Further data processing...

¿Qué ocurre aquí?  En primer lugar, tenemos que identificar nuestros pares de divisas. Ahora mismo solo tendemos EURUSD, pero podremos añadir otros. A continuación, fijaremos el intervalo temporal. H4 son 4 horas. Este es la opción óptima en cuanto al tiempo. 

A continuación, las fechas. De 1990 a 2024. Necesitaremos muchas cotizaciones históricas. Cuantos más datos tengamos, más precisos serán nuestros análisis. Ahora a lo principal: la función process_currency_pair. Esta carga los datos utilizando copy_rates_range.

¿Qué obtendremos finalmente? Un DataFrame con datos históricos. Hora, apertura, cierre, máximo, mínimo: todo lo que necesitamos para hacer el trabajo.

En caso de que algo vaya mal, se identificarán los errores, se mostrarán y se volverá a intentarlo.


Procesamiento de series temporales: conversión de datos OHLC en direcciones de precios

Volvamos a nuestra tarea principal: la conversión de las fluctuaciones caóticas del mercado de divisas en algo más ordenado: tendencias e inversiones. ¿Cómo lo haremos? Según un esquema bastante comprensible: convertiremos los precios en direcciones.

Aquí está nuestro código:

# Fill missing values with the mean
ohlc_data.fillna(ohlc_data.mean(), inplace=True)

# Convert price movements to directions
ohlc_data['direction'] = np.where(ohlc_data['close'].diff() > 0, 'up', 'down')

¿Qué ocurre aquí? En primer lugar, rellenaremos los espacios en blanco. Estos pueden empeorar mucho el resultado final. Los rellenaremos con medias. 

Y ahora viene la parte divertida. Crearemos una nueva columna "dirección". Con esta columna, convertiremos los datos de precios en datos que imiten el comportamiento tendencial. Funciona de forma muy simple:

  • Si el precio de cierre actual es superior al anterior, se escribirá "up".
  • Si es inferior, se escribirá "down".

Una formulación bastante simple, pero bastante eficaz. Ahora, en lugar de números complejos, tendremos una simple secuencia de "up" y "down". Esta secuencia será mucho más fácil de percibir. Pero, ¿para qué queremos esto? Estos "up" y "down" son los componentes básicos de nuestros patrones. A partir de ellos obtendremos una imagen completa de lo que sucede en el mercado.


Algoritmo de búsqueda de patrones: función find_patterns

Así que tenemos una secuencia de "up" y "down". A continuación, buscaremos patrones repetitivos en esta secuencia formas que se repitan (patrones).

Aquí tenemos la función find_patterns:

def find_patterns(data, pattern_length, direction):
    patterns = defaultdict(list)
    last_pattern = None
    last_winrate = None
    last_frequency = None

    for i in range(len(data) - pattern_length - 6):
        pattern = tuple(data['direction'][i:i+pattern_length])
        if data['direction'][i+pattern_length+6] == direction:
            patterns[pattern].append(True)
        else:
            patterns[pattern].append(False)

    # Check last prices for pattern match
    last_pattern_tuple = tuple(data['direction'][-pattern_length:])
    if last_pattern_tuple in patterns:
        last_winrate = np.mean(patterns[last_pattern_tuple]) * 100
        last_frequency = len(patterns[last_pattern_tuple])
        last_pattern = last_pattern_tuple

    return patterns, last_pattern, last_winrate, last_frequency

¿Cómo funciona todo esto?

  • Estamos construyendo el diccionario patterns. Esto servirá como una especie de biblioteca donde almacenaremos todos los patrones que encontremos.
  • Entonces empezaremos a revisar los datos. Tomaremos una muestra de datos de pattern_length (puede ser 3, 4, 5 y así hasta 25) y veremos qué ocurre 6 barras después de ella.
  • Si después de 6 barras el precio se mueve en la dirección deseada (es decir, hacia arriba para patrones de compra o hacia abajo para patrones de venta), registraremos True. Si no, False.
  • Haremos esto para todos los ejemplos de datos posibles. Deberíamos obtener patrones similares: "up-up-down" - True, "down-up" - False y así sucesivamente.
  • A continuación, comprobaremos si se está formando algún patrón que se haya detectado anteriormente. En caso afirmativo, calcularemos su tasa de victorias (porcentaje de procesamientos con éxito) y su frecuencia de aparición.

Así es como convertiremos una simple secuencia de "up" y "down" en una herramienta de previsión bastante eficaz. Pero eso no es todo. Después filtraremos los patrones, seleccionaremos los más eficaces y los analizaremos.


Cálculo estadístico de los patrones: WinRate y frecuencia de aparición

Ahora que disponemos de un cierto número de patrones, tendremos que seleccionar los mejores.

Echemos un vistazo a nuestro código:

def calculate_winrate_and_frequency(patterns):
    results = []
    for pattern, outcomes in patterns.items():
        winrate = np.mean(outcomes) * 100
        frequency = len(outcomes)
        results.append((pattern, winrate, frequency))
    results.sort(key=lambda x: x[1], reverse=True)
    return results

Aquí tomaremos cada patrón y sus resultados (los mencionamos antes como True y False) y luego calcularemos el winrate, que es nuestro porcentaje de productividad. Si un patrón funciona 7 de cada 10 veces, tendrá una tasa de ganancia del 70%. También calcularemos la frecuencia, que es el número de veces que se ha encontrado el patrón. Cuanto más a menudo se halle, más fiables serán nuestras estadísticas. Lo pondremos todo en la lista de resultados. Y al final, la clasificación. Pondremos los mejores patrones al principio de la lista.


Filtrado de resultados: selección de patrones significativos

Ahora tenemos una buena cantidad de datos. Pero no los necesitaremos todos, hay que filtrarlos.

filtered_buy_results = [result for result in all_buy_results if result[2] > 20]
filtered_sell_results = [result for result in all_sell_results if result[2] > 20]

filtered_buy_results.sort(key=lambda x: x[1], reverse=True)
top_300_buy_patterns = filtered_buy_results[:300]

filtered_sell_results.sort(key=lambda x: x[1], reverse=True)
top_300_sell_patterns = filtered_sell_results[:300]

De forma similar, configuraremos el filtrado. En primer lugar, eliminaremos todos los patrones que aparecen menos de 20 veces. Las estadísticas muestran que los patrones poco frecuentes son menos fiables. 

A continuación, clasificaremos los patrones restantes según la tasa de victorias. Los más eficaces figurarán al principio de la lista. Al final, elegiremos los 300 mejores. Eso es todo lo que debería quedar de la multitud de patrones, que superan el millar.  


Trabajo con diferentes longitudes de patrón: de 3 a 25

Ahora necesitaremos seleccionar variaciones de patrones que estadísticamente y regularmente generen ganancias al negociar. Las variantes diferirán en longitud. Pueden constar de 3 ó 25 movimientos de precios. Comprobaremos todas las posibles:

pattern_lengths = range(3, 25)  # Pattern lengths from 3 to 25
all_buy_patterns = {}
all_sell_patterns = {}

for pattern_length in pattern_lengths:
    buy_patterns, last_buy_pattern, last_buy_winrate, last_buy_frequency = find_patterns(ohlc_data, pattern_length, 'up')
    sell_patterns, last_sell_pattern, last_sell_winrate, last_sell_frequency = find_patterns(ohlc_data, pattern_length, 'down')
    all_buy_patterns[pattern_length] = buy_patterns
    all_sell_patterns[pattern_length] = sell_patterns

Luego ejecutaremos nuestro filtro de búsqueda de patrones para cada longitud de 3 a 25. ¿Por qué? Los patrones de menos de tres movimientos resultan poco fiables: ya lo hemos mencionado antes. Y más de 25 será muy raro. Para cada longitud, buscaremos patrones tanto de compra como de venta. 

Pero, ¿por qué necesitamos tantas longitudes distintas? Los patrones cortos pueden captar los cambios rápidos del mercado, mientras que los largos pueden mostrar las tendencias a largo plazo. Aún no sabemos de antemano qué será más eficaz, así que lo probaremos todo.


Análisis de patrones de compra y venta

Ahora que tenemos un conjunto específico de patrones de distintas longitudes, deberemos determinar cuáles funcionan realmente. 

Aquí está nuestro código en acción:

all_buy_results = []
for pattern_length, patterns in all_buy_patterns.items():
    results = calculate_winrate_and_frequency(patterns)
    all_buy_results.extend(results)

all_sell_results = []
for pattern_length, patterns in all_sell_patterns.items():
    results = calculate_winrate_and_frequency(patterns)
    all_sell_results.extend(results)

Tomaremos cada patrón (tanto de compra como de venta) y lo filtraremos a través de nuestra función de cálculo de tasas de victorias y frecuencia. 

Pero no solo calcularemos las estadísticas. Buscaremos la diferencia entre los patrones de compra y de venta. Por qué esto es tan importante. Porque el mercado puede comportarse de forma distinta cuando sube y cuando baja. A veces las patrones de compra funcionan mejor, y a veces los patrones de venta.

Luego procederemos al siguiente paso, que consistirá en comparar patrones de distinta longitud entre sí. Puede resultar que las patrones cortos funcionen mejor para determinar un punto de entrada en el mercado, mientras que las patrones largos funcionan mejor para determinar una tendencia a largo plazo. También puede ocurrir al contrario. Por eso lo analizaremos todo y no descartaremos nada de antemano.

Al final de este análisis tendremos los primeros resultados: qué patrones funcionan mejor para comprar, qué patrones funcionan mejor para vender y qué longitud de patrón es más eficaz en diferentes condiciones de mercado. Con estos datos ya podremos analizar los precios en el mercado de divisas.

Pero recuerden amigos, ni siquiera el mejor patrón supone una garantía de éxito. El mercado está lleno de sorpresas. Nuestro trabajo consistirá en aumentar las posibilidades de éxito, y lo haremos analizando los patrones desde todos los ángulos.


Mirando al futuro: previsiones basadas en patrones recientes

Ahora es el momento de hacer predicciones concretas. Echemos un vistazo a nuestro código-adivinador:

if last_buy_pattern:
    print(f"\nLast buy pattern for {symbol}: {last_buy_pattern}, Winrate: {last_buy_winrate:.2f}%, Frequency: {last_buy_frequency}")
    print(f"Forecast: Price will likely go up.")
if last_sell_pattern:
    print(f"\nLast sell pattern for {symbol}: {last_sell_pattern}, Winrate: {last_sell_winrate:.2f}%, Frequency: {last_sell_frequency}")
    print(f"Forecast: Price will likely go down.")

Observaremos el último patrón formado e intentaremos predecir el futuro. Nos fijaremos en el último patrón formado y haremos nuestro análisis comercial.

Tenga en cuenta que estamos viendo dos escenarios: un patrón de compra y un patrón de venta. ¿Por qué? Porque el mercado es un continuo enfrentamiento entre toros y osos, compradores y vendedores. Debemos estar preparados para cualquier giro de los eventos.

Para cada patrón, obtendremos tres parámetros clave: el patrón en sí, su tasa de victorias y su frecuencia de aparición. La tasa de victorias resulta especialmente importante. Si un patrón de compra tiene una tasa de victorias del 70%, significará que en el 70% de los casos después de la aparición de este patrón, el precio realmente ha subido. Son resultados bastante buenos. Pero recuerde que ni siquiera el 90% supone una garantía. En el mundo del Fórex, siempre hay lugar para lo inesperado.

La frecuencia también juega un papel importante. Un patrón que se produce con frecuencia resulta más fiable que un patrón poco frecuente.

La parte más interesante será nuestra previsión. "Es probable que el precio suba" o "Es probable que el precio baje".  Estas predicciones aportan cierta satisfacción por el trabajo realizado. Sin embargo, recuerden, amigos míos, que incluso la predicción más exacta supone solo una probabilidad, no una garantía. El mercado de divisas es bastante difícil de predecir. Las noticias, los acontecimientos económicos e incluso los tweets de personas influyentes pueden cambiar la dirección de movimiento de los precios en cuestión de segundos.

Por consiguiente, nuestro código no será una panacea, sino más bien un asesor muy inteligente. Puede interpretarse así: "Mire, basándonos en los datos históricos, tenemos razones para creer que el precio subirá (o bajará)". A usted le corresponderá decidir si entra o no en el mercado. El uso de estas previsiones será un proceso reflexivo. Usted dispondrá de información sobre los posibles movimientos, pero cada movimiento deberá realizarse con prudencia, considerando la situación general del mercado.


Dibujando el futuro: la visualización de los mejores patrones y previsiones

Vamos a añadir un poco de magia de visualización a nuestro código:

import matplotlib.pyplot as plt

def visualize_patterns(patterns, title, filename):
    patterns = patterns[:20]  # Берем топ-20 для наглядности
    patterns.reverse()  # Разворачиваем список для правильного отображения на графике

    fig, ax = plt.subplots(figsize=(12, 8))
    
    winrates = [p[1] for p in patterns]
    frequencies = [p[2] for p in patterns]
    labels = [' '.join(p[0]) for p in patterns]

    ax.barh(range(len(patterns)), winrates, align='center', color='skyblue', zorder=10)
    ax.set_yticks(range(len(patterns)))
    ax.set_yticklabels(labels)
    ax.invert_yaxis()  # Инвертируем ось Y для отображения лучших паттернов сверху

    ax.set_xlabel('Winrate (%)')
    ax.set_title(title)

    # Добавляем частоту встречаемости
    for i, v in enumerate(winrates):
        ax.text(v + 1, i, f'Freq: {frequencies[i]}', va='center')

    plt.tight_layout()
    plt.savefig(filename)
    plt.close()

# Визуализируем топ паттерны для покупки и продажи
visualize_patterns(top_300_buy_patterns, f'Top 20 Buy Patterns for {symbol}', 'top_buy_patterns.png')
visualize_patterns(top_300_sell_patterns, f'Top 20 Sell Patterns for {symbol}', 'top_sell_patterns.png')

# Визуализируем последний паттерн и прогноз
def visualize_forecast(pattern, winrate, frequency, direction, symbol, filename):
    fig, ax = plt.subplots(figsize=(8, 6))
    
    ax.bar(['Winrate'], [winrate], color='green' if direction == 'up' else 'red')
    ax.set_ylim(0, 100)
    ax.set_ylabel('Winrate (%)')
    ax.set_title(f'Forecast for {symbol}: Price will likely go {direction}')

    ax.text(0, winrate + 5, f'Pattern: {" ".join(pattern)}', ha='center')
    ax.text(0, winrate - 5, f'Frequency: {frequency}', ha='center')

    plt.tight_layout()
    plt.savefig(filename)
    plt.close()

if last_buy_pattern:
    visualize_forecast(last_buy_pattern, last_buy_winrate, last_buy_frequency, 'up', symbol, 'buy_forecast.png')
if last_sell_pattern:
    visualize_forecast(last_sell_pattern, last_sell_winrate, last_sell_frequency, 'down', symbol, 'sell_forecast.png')

Crearemos dos funciones: visualize_patterns y visualize_forecast. El primero dibujará un gráfico de barras horizontales informativo con los 20 patrones principales, sus porcentajes de victorias y la frecuencia de aparición. El segundo creará una representación visual de nuestra predicción basada en el último patrón.

Usaremos columnas horizontales para los patrones, porque los patrones pueden ser largos y resulta más fácil leerlos así. Tendremos un color celeste muy agradable al ojo humano.

Guardaremos nuestras obras maestras en archivos PNG.


Pruebas y backtesting del sistema de análisis de patrones

Hemos creado nuestro sistema de análisis de patrones, pero ¿cómo sabremos si realmente funciona? Para ello, deberemos probarlo con datos históricos. 

Aquí está el código necesario para esta tarea:

def simulate_trade(data, direction, entry_price, take_profit, stop_loss):
    for i, row in data.iterrows():
        current_price = row['close']
        
        if direction == "BUY":
            if current_price >= entry_price + take_profit:
                return {'profit': take_profit, 'duration': i}
            elif current_price <= entry_price - stop_loss:
                return {'profit': -stop_loss, 'duration': i}
        else:  # SELL
            if current_price <= entry_price - take_profit:
                return {'profit': take_profit, 'duration': i}
            elif current_price >= entry_price + stop_loss:
                return {'profit': -stop_loss, 'duration': i}
    
    # Если цикл завершился без достижения TP или SL, закрываем по текущей цене
    last_price = data['close'].iloc[-1]
    profit = (last_price - entry_price) if direction == "BUY" else (entry_price - last_price)
    return {'profit': profit, 'duration': len(data)}

def backtest_pattern_system(data, buy_patterns, sell_patterns):
    equity_curve = [10000]  # Начальный капитал $10,000
    trades = []
    
    for i in range(len(data) - max(len(p[0]) for p in buy_patterns + sell_patterns)):
        current_data = data.iloc[:i+1]
        last_pattern = tuple(current_data['direction'].iloc[-len(buy_patterns[0][0]):])
        
        matching_buy = [p for p in buy_patterns if p[0] == last_pattern]
        matching_sell = [p for p in sell_patterns if p[0] == last_pattern]
        
        if matching_buy and not matching_sell:
            entry_price = current_data['close'].iloc[-1]
            take_profit = 0.001  # 10 pips
            stop_loss = 0.0005  # 5 pips
            trade_result = simulate_trade(data.iloc[i+1:], "BUY", entry_price, take_profit, stop_loss)
            trades.append(trade_result)
            equity_curve.append(equity_curve[-1] + trade_result['profit'] * 10000)  # Умножаем на 10000 для конвертации в доллары
        elif matching_sell and not matching_buy:
            entry_price = current_data['close'].iloc[-1]
            take_profit = 0.001  # 10 pips
            stop_loss = 0.0005  # 5 pips
            trade_result = simulate_trade(data.iloc[i+1:], "SELL", entry_price, take_profit, stop_loss)
            trades.append(trade_result)
            equity_curve.append(equity_curve[-1] + trade_result['profit'] * 10000)  # Умножаем на 10000 для конвертации в доллары
        else:
            equity_curve.append(equity_curve[-1])
    
    return equity_curve, trades

# Проводим бэктест
equity_curve, trades = backtest_pattern_system(ohlc_data, top_300_buy_patterns, top_300_sell_patterns)

# Визуализация результатов бэктеста
plt.figure(figsize=(12, 6))
plt.plot(equity_curve)
plt.title('Equity Curve')
plt.xlabel('Trades')
plt.ylabel('Equity ($)')
plt.savefig('equity_curve.png')
plt.close()

# Расчет статистики бэктеста
total_profit = equity_curve[-1] - equity_curve[0]
win_rate = sum(1 for trade in trades if trade['profit'] > 0) / len(trades) if trades else 0
average_profit = sum(trade['profit'] for trade in trades) / len(trades) if trades else 0

print(f"\nBacktest Results:")
print(f"Total Profit: ${total_profit:.2f}")
print(f"Win Rate: {win_rate:.2%}")
print(f"Average Profit per Trade: ${average_profit*10000:.2f}")
print(f"Total Trades: {len(trades)}")

¿Qué ocurre aquí? La función simulate_trade será nuestro simulador de una operación individual. Supervisará el precio y cerrará una transacción cuando se alcance el take profit o el stop loss. 

backtest_pattern_system, por su parte, será la función más importante. Se encargará de iterar los datos históricos, paso a paso, día a día, comprobando si se ha formado uno de nuestros patrones. ¿Ha encontrado un patrón de compra? Pues compramos. ¿Ha encontrado un patrón de venta? Pues vendemos.

Usaremos un take profit fijo de 100 pips y un stop loss de 50 pips, ya que debemos poner límites a los beneficios satisfactorios: no demasiado para no arriesgar por encima del límite, pero tampoco demasiado poco para permitir que aumenten los beneficios.

Después de cada operación, actualizaremos nuestra curva de capital. Al final de nuestro trabajo obtendremos este resultado: cuánto hemos ganado en total, qué porcentaje de transacciones ha sido rentable y qué beneficio medio hemos obtenido por transacción. Y, por supuesto, visualizaremos los resultados.

Vamos a implementar la búsqueda de patrones usando el lenguaje MQL5. Aquí está nuestro código:

//+------------------------------------------------------------------+
//|                                       PatternProbabilityIndicator|
//|                                                 Copyright 2024   |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, Your Name Here"
#property link      "https://www.mql5.com"
#property version   "1.06"
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2

//--- plot BuyProbability
#property indicator_label1  "BuyProbability"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrGreen
#property indicator_style1  STYLE_SOLID
#property indicator_width1  2

//--- plot SellProbability
#property indicator_label2  "SellProbability"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrRed
#property indicator_style2  STYLE_SOLID
#property indicator_width2  2

//--- input parameters
input int      InpPatternLength = 5;    // Pattern Length (3-10)
input int      InpLookback     = 1000;  // Lookback Period (100-5000)
input int      InpForecastHorizon = 6;  // Forecast Horizon (1-20)

//--- indicator buffers
double         BuyProbabilityBuffer[];
double         SellProbabilityBuffer[];

//--- global variables
int            g_pattern_length;
int            g_lookback;
int            g_forecast_horizon;
string         g_patterns[];
int            g_pattern_count;
int            g_pattern_occurrences[];
int            g_pattern_successes[];
int            g_total_bars;

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
   //--- validate inputs
   if(InpPatternLength < 3 || InpPatternLength > 10)
   {
      Print("Invalid Pattern Length. Must be between 3 and 10.");
      return INIT_PARAMETERS_INCORRECT;
   }
   
   if(InpLookback < 100 || InpLookback > 5000)
   {
      Print("Invalid Lookback Period. Must be between 100 and 5000.");
      return INIT_PARAMETERS_INCORRECT;
   }

   if(InpForecastHorizon < 1 || InpForecastHorizon > 20)
   {
      Print("Invalid Forecast Horizon. Must be between 1 and 20.");
      return INIT_PARAMETERS_INCORRECT;
   }

   //--- indicator buffers mapping
   SetIndexBuffer(0, BuyProbabilityBuffer, INDICATOR_DATA);
   SetIndexBuffer(1, SellProbabilityBuffer, INDICATOR_DATA);
   
   //--- set accuracy
   IndicatorSetInteger(INDICATOR_DIGITS, 2);
   
   //--- set global variables
   g_pattern_length = InpPatternLength;
   g_lookback = InpLookback;
   g_forecast_horizon = InpForecastHorizon;
   
   //--- generate all possible patterns
   if(!GeneratePatterns())
   {
      Print("Failed to generate patterns.");
      return INIT_FAILED;
   }
   
   g_total_bars = iBars(_Symbol, PERIOD_CURRENT);
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
   //--- check for rates total
   if(rates_total <= g_lookback + g_pattern_length + g_forecast_horizon)
   {
      Print("Not enough data for calculation.");
      return 0;
   }

   int start = (prev_calculated > g_lookback + g_pattern_length + g_forecast_horizon) ? 
               prev_calculated - 1 : g_lookback + g_pattern_length + g_forecast_horizon;
   
   if(ArraySize(g_pattern_occurrences) != g_pattern_count)
   {
      ArrayResize(g_pattern_occurrences, g_pattern_count);
      ArrayResize(g_pattern_successes, g_pattern_count);
   }
   
   ArrayInitialize(g_pattern_occurrences, 0);
   ArrayInitialize(g_pattern_successes, 0);
   
   // Pre-calculate patterns for efficiency
   string patterns[];
   ArrayResize(patterns, rates_total);
   for(int i = g_pattern_length; i < rates_total; i++)
   {
      patterns[i] = "";
      for(int j = 0; j < g_pattern_length; j++)
      {
         patterns[i] += (close[i-j] > close[i-j-1]) ? "U" : "D";
      }
   }
   
   // Main calculation loop
   for(int i = start; i < rates_total; i++)
   {
      string current_pattern = patterns[i];
      
      if(StringLen(current_pattern) != g_pattern_length) continue;
      
      double buy_probability = CalculateProbability(current_pattern, true, close, patterns, i);
      double sell_probability = CalculateProbability(current_pattern, false, close, patterns, i);
      
      BuyProbabilityBuffer[i] = buy_probability;
      SellProbabilityBuffer[i] = sell_probability;
   }
   
   // Update Comment with pattern statistics if total bars changed
   if(g_total_bars != iBars(_Symbol, PERIOD_CURRENT))
   {
      g_total_bars = iBars(_Symbol, PERIOD_CURRENT);
      UpdatePatternStatistics();
   }
   
   return(rates_total);
}

//+------------------------------------------------------------------+
//| Generate all possible patterns                                   |
//+------------------------------------------------------------------+
bool GeneratePatterns()
{
   g_pattern_count = (int)MathPow(2, g_pattern_length);
   if(!ArrayResize(g_patterns, g_pattern_count))
   {
      Print("Failed to resize g_patterns array.");
      return false;
   }
   
   for(int i = 0; i < g_pattern_count; i++)
   {
      string pattern = "";
      for(int j = 0; j < g_pattern_length; j++)
      {
         pattern += ((i >> j) & 1) ? "U" : "D";
      }
      g_patterns[i] = pattern;
   }
   
   return true;
}

//+------------------------------------------------------------------+
//| Calculate probability for a given pattern                        |
//+------------------------------------------------------------------+
double CalculateProbability(const string &pattern, bool is_buy, const double &close[], const string &patterns[], int current_index)
{
   if(StringLen(pattern) != g_pattern_length || current_index < g_lookback)
   {
      return 50.0; // Return neutral probability on error
   }

   int pattern_index = ArraySearch(g_patterns, pattern);
   if(pattern_index == -1)
   {
      return 50.0;
   }

   int total_occurrences = 0;
   int successful_predictions = 0;
   
   for(int i = g_lookback; i > g_pattern_length + g_forecast_horizon; i--)
   {
      int historical_index = current_index - i;
      if(historical_index < 0 || historical_index + g_pattern_length + g_forecast_horizon >= ArraySize(close))
      {
         continue;
      }

      if(patterns[historical_index] == pattern)
      {
         total_occurrences++;
         g_pattern_occurrences[pattern_index]++;
         if(is_buy && close[historical_index + g_pattern_length + g_forecast_horizon] > close[historical_index + g_pattern_length])
         {
            successful_predictions++;
            g_pattern_successes[pattern_index]++;
         }
         else if(!is_buy && close[historical_index + g_pattern_length + g_forecast_horizon] < close[historical_index + g_pattern_length])
         {
            successful_predictions++;
            g_pattern_successes[pattern_index]++;
         }
      }
   }
   
   return (total_occurrences > 0) ? (double)successful_predictions / total_occurrences * 100 : 50;
}

//+------------------------------------------------------------------+
//| Update pattern statistics and display in Comment                 |
//+------------------------------------------------------------------+
void UpdatePatternStatistics()
{
   string comment = "Pattern Statistics:\n";
   comment += "Pattern Length: " + IntegerToString(g_pattern_length) + "\n";
   comment += "Lookback Period: " + IntegerToString(g_lookback) + "\n";
   comment += "Forecast Horizon: " + IntegerToString(g_forecast_horizon) + "\n\n";
   comment += "Top 5 Patterns:\n";
   
   int sorted_indices[];
   ArrayResize(sorted_indices, g_pattern_count);
   for(int i = 0; i < g_pattern_count; i++) sorted_indices[i] = i;
   
   // Use quick sort for better performance
   ArraySort(sorted_indices);
   
   for(int i = 0; i < 5 && i < g_pattern_count; i++)
   {
      int idx = sorted_indices[g_pattern_count - 1 - i];  // Reverse order for descending sort
      double win_rate = g_pattern_occurrences[idx] > 0 ? 
                        (double)g_pattern_successes[idx] / g_pattern_occurrences[idx] * 100 : 0;
      
      comment += g_patterns[idx] + ": " +
                 "Occurrences: " + IntegerToString(g_pattern_occurrences[idx]) + ", " +
                 "Win Rate: " + DoubleToString(win_rate, 2) + "%\n";
   }
   
   Comment(comment);
}

//+------------------------------------------------------------------+
//| Custom function to search for a string in an array               |
//+------------------------------------------------------------------+
int ArraySearch(const string &arr[], string value)
{
   for(int i = 0; i < ArraySize(arr); i++)
   {
      if(arr[i] == value) return i;
   }
   return -1;
}

Cómo se ve en el gráfico:


Creación de un asesor experto para la detección de patrones y la negociación

A continuación, comprobaremos los desarrollos en el simulador de MetaTrader 5, ya que las pruebas de Python se han realizado correctamente. Publicamos el código más abajo y también lo adjuntamos al artículo. El código supondrá una aplicación práctica del concepto de análisis de patrones en el mercado de divisas, y encarnará la idea de que los patrones históricos de precios pueden ofrecer información estadísticamente significativa sobre los futuros movimientos del mercado.

Componentes clave del asesor experto:

  • Generación de patrones: El EA utilizará una representación binaria de los movimientos del precio (al alza o a la baja), creando todas las combinaciones posibles para una longitud de patrón determinada.
  • Análisis estadísticos: El EA realizará un análisis retrospectivo, evaluando la frecuencia de aparición de cada patrón y su rendimiento predictivo.
  • Adaptación dinámica: El EA actualizará constantemente las estadísticas de los patrones para adaptarse a las condiciones cambiantes del mercado.
  • Toma de decisiones comerciales: Basándose en los patrones identificados más eficaces para la compra y la venta, el asesor experto abrirá, cerrará o mantendrá las posiciones.
  • Parametrización: El EA ofrecerá la posibilidad de configurar parámetros clave como la longitud del patrón, el periodo de análisis, el horizonte de previsión y el número mínimo de apariciones de un patrón para contabilizarlo.

En total, hemos implementado 4 variantes del asesor experto: la primera según el concepto del artículo, abrirá transacciones según los patrones, y cerrará cuando se encuentre un nuevo patrón mejor en la dirección opuesta. La segunda será la misma, pero multidivisa: funcionará con los 10 pares de divisas más líquidos, según las estadísticas del Banco Mundial. La tercera será igual, pero cerrará las transacciones cuando el precio supere un número de barras superior al horizonte de previsión. Y la última cerrará usando take profit y stop loss.

Aquí está el código del primer EA, el resto estará en los archivos adjuntos:

//+------------------------------------------------------------------+
//|                                  PatternProbabilityExpertAdvisor |
//|                                Copyright 2024, Evgeniy Koshtenko |
//|                          https://www.mql5.com/ru/users/koshtenko |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, Evgeniy Koshtenko"
#property link      "https://www.mql5.com/ru/users/koshtenko"
#property version   "1.00"

#include <Trade\Trade.mqh>            // Подключаем торговый класс CTrade

//--- input parameters
input int      InpPatternLength = 5;    // Pattern Length (3-10)
input int      InpLookback     = 1000;  // Lookback Period (100-5000)
input int      InpForecastHorizon = 6;  // Forecast Horizon (1-20)
input double   InpLotSize = 0.1;        // Lot Size
input int      InpMinOccurrences = 30;  // Minimum Pattern Occurrences

//--- global variables
int            g_pattern_length;
int            g_lookback;
int            g_forecast_horizon;
string         g_patterns[];
int            g_pattern_count;
int            g_pattern_occurrences[];
int            g_pattern_successes[];
int            g_total_bars;
string         g_best_buy_pattern = "";
string         g_best_sell_pattern = "";

CTrade trade;                         // Используем торговый класс CTrade
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   //--- validate inputs
   if(InpPatternLength < 3 || InpPatternLength > 10)
   {
      Print("Invalid Pattern Length. Must be between 3 and 10.");
      return INIT_PARAMETERS_INCORRECT;
   }
   
   if(InpLookback < 100 || InpLookback > 5000)
   {
      Print("Invalid Lookback Period. Must be between 100 and 5000.");
      return INIT_PARAMETERS_INCORRECT;
   }

   if(InpForecastHorizon < 1 || InpForecastHorizon > 20)
   {
      Print("Invalid Forecast Horizon. Must be between 1 and 20.");
      return INIT_PARAMETERS_INCORRECT;
   }

   //--- set global variables
   g_pattern_length = InpPatternLength;
   g_lookback = InpLookback;
   g_forecast_horizon = InpForecastHorizon;
   
   //--- generate all possible patterns
   if(!GeneratePatterns())
   {
      Print("Failed to generate patterns.");
      return INIT_FAILED;
   }
   
   g_total_bars = iBars(_Symbol, PERIOD_CURRENT);
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   if(!IsNewBar()) return;
   
   UpdatePatternStatistics();
   
   string current_pattern = GetCurrentPattern();
   
   if(current_pattern == g_best_buy_pattern)
   {
      if(PositionSelect(_Symbol) && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
      {
         trade.PositionClose(_Symbol);
      }
      if(!PositionSelect(_Symbol))
      {
         trade.Buy(InpLotSize, _Symbol, 0, 0, 0, "Buy Pattern: " + current_pattern);
      }
   }
   else if(current_pattern == g_best_sell_pattern)
   {
      if(PositionSelect(_Symbol) && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
      {
         trade.PositionClose(_Symbol);
      }
      if(!PositionSelect(_Symbol))
      {
         trade.Sell(InpLotSize, _Symbol, 0, 0, 0, "Sell Pattern: " + current_pattern);
      }
   }
}

//+------------------------------------------------------------------+
//| Generate all possible patterns                                   |
//+------------------------------------------------------------------+
bool GeneratePatterns()
{
   g_pattern_count = (int)MathPow(2, g_pattern_length);
   if(!ArrayResize(g_patterns, g_pattern_count))
   {
      Print("Failed to resize g_patterns array.");
      return false;
   }
   
   for(int i = 0; i < g_pattern_count; i++)
   {
      string pattern = "";
      for(int j = 0; j < g_pattern_length; j++)
      {
         pattern += ((i >> j) & 1) ? "U" : "D";
      }
      g_patterns[i] = pattern;
   }
   
   return true;
}

//+------------------------------------------------------------------+
//| Update pattern statistics and find best patterns                 |
//+------------------------------------------------------------------+
void UpdatePatternStatistics()
{
   if(ArraySize(g_pattern_occurrences) != g_pattern_count)
   {
      ArrayResize(g_pattern_occurrences, g_pattern_count);
      ArrayResize(g_pattern_successes, g_pattern_count);
   }
   
   ArrayInitialize(g_pattern_occurrences, 0);
   ArrayInitialize(g_pattern_successes, 0);
   
   int total_bars = iBars(_Symbol, PERIOD_CURRENT);
   int start = total_bars - g_lookback;
   if(start < g_pattern_length + g_forecast_horizon) start = g_pattern_length + g_forecast_horizon;
   
   double close[];
   ArraySetAsSeries(close, true);
   CopyClose(_Symbol, PERIOD_CURRENT, 0, total_bars, close);
   
   string patterns[];
   ArrayResize(patterns, total_bars);
   ArraySetAsSeries(patterns, true);
   
   for(int i = 0; i < total_bars - g_pattern_length; i++)
   {
      patterns[i] = "";
      for(int j = 0; j < g_pattern_length; j++)
      {
         patterns[i] += (close[i+j] > close[i+j+1]) ? "U" : "D";
      }
   }
   
   for(int i = start; i >= g_pattern_length + g_forecast_horizon; i--)
   {
      string current_pattern = patterns[i];
      int pattern_index = ArraySearch(g_patterns, current_pattern);
      
      if(pattern_index != -1)
      {
         g_pattern_occurrences[pattern_index]++;
         if(close[i-g_forecast_horizon] > close[i])
         {
            g_pattern_successes[pattern_index]++;
         }
      }
   }
   
   double best_buy_win_rate = 0;
   double best_sell_win_rate = 0;
   
   for(int i = 0; i < g_pattern_count; i++)
   {
      if(g_pattern_occurrences[i] >= InpMinOccurrences)
      {
         double win_rate = (double)g_pattern_successes[i] / g_pattern_occurrences[i];
         if(win_rate > best_buy_win_rate)
         {
            best_buy_win_rate = win_rate;
            g_best_buy_pattern = g_patterns[i];
         }
         if((1 - win_rate) > best_sell_win_rate)
         {
            best_sell_win_rate = 1 - win_rate;
            g_best_sell_pattern = g_patterns[i];
         }
      }
   }
   
   Print("Best Buy Pattern: ", g_best_buy_pattern, " (Win Rate: ", DoubleToString(best_buy_win_rate * 100, 2), "%)");
   Print("Best Sell Pattern: ", g_best_sell_pattern, " (Win Rate: ", DoubleToString(best_sell_win_rate * 100, 2), "%)");
}

//+------------------------------------------------------------------+
//| Get current price pattern                                        |
//+------------------------------------------------------------------+
string GetCurrentPattern()
{
   double close[];
   ArraySetAsSeries(close, true);
   CopyClose(_Symbol, PERIOD_CURRENT, 0, g_pattern_length + 1, close);
   
   string pattern = "";
   for(int i = 0; i < g_pattern_length; i++)
   {
      pattern += (close[i] > close[i+1]) ? "U" : "D";
   }
   
   return pattern;
}

//+------------------------------------------------------------------+
//| Custom function to search for a string in an array               |
//+------------------------------------------------------------------+
int ArraySearch(const string &arr[], string value)
{
   for(int i = 0; i < ArraySize(arr); i++)
   {
      if(arr[i] == value) return i;
   }
   return -1;
}

//+------------------------------------------------------------------+
//| Check if it's a new bar                                          |
//+------------------------------------------------------------------+
bool IsNewBar()
{
   static datetime last_time = 0;
   datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0);
   if(current_time != last_time)
   {
      last_time = current_time;
      return true;
   }
   return false;
}

En cuanto a los resultados de la prueba, en el euro-dólar son los siguientes:

Con detalle:

No está mal, y los gráficos son preciosos. Las otras versiones de EAs o deambulan en cero o nos llevan a reducciones prolongadas. Ah y la mejor opción no se ajusta del todo a mis criterios, me gustan los EAs con un factor de beneficio superior a 2 y un ratio de Sharpe superior a 1. Automáticamente se me ha ocurrido que el simulador de Python debería haber considerado tanto la comisión de transacción como el spread y el swap y, en general, para hacer un simulador normal....


Posibles mejoras: ampliar el marco temporal y añadir indicadores

Continuamos nuestras reflexiones. No cabe duda de que el sistema ofrece resultados positivos, pero ¿cómo mejorarlos? ¿Estamos siendo realistas o no?

Ahora nos fijaremos en el marco temporal de 4 horas. Intentemos mirar más allá, añadiendo un gráfico diario, semanal o incluso mensual. Con este enfoque, podremos ver tendencias más globales, patrones más amplios. Vamos a desplegar el código para abarcar todas estas escalas temporales:

timeframes = [mt5.TIMEFRAME_H4, mt5.TIMEFRAME_D1, mt5.TIMEFRAME_W1, mt5.TIMEFRAME_MN1]
for tf in timeframes:
    ohlc_data = get_ohlc_data(symbol, tf, start_date, end_date)
    patterns = find_patterns(ohlc_data)

Más datos, más ruido. Tendremos que aprender a filtrar este ruido para obtener datos más claros.

Vamos a ampliar la gama de características analizadas. En el mundo del trading, será la incorporación de indicadores técnicos. RSI, MACD y Bandas de Bollinger son las herramientas más utilizadas.

def add_indicators(data):
    data['RSI'] = ta.RSI(data['close'])
    data['MACD'] = ta.MACD(data['close']).macd()
    data['BB_upper'], data['BB_middle'], data['BB_lower'] = ta.BBANDS(data['close'])
    return data

ohlc_data = add_indicators(ohlc_data)

Los indicadores pueden ayudarnos a confirmar las señales de nuestras patrones. También podemos buscar patrones en los indicadores.


Conclusión

Aquí hemos terminado nuestro trabajo de búsqueda y análisis de patrones. Hemos creado un sistema que busca patrones en el caos del mercado. También hemos aprendido a visualizar nuestros resultados, a realizar pruebas retrospectivas y a planificar futuras mejoras. Pero lo más importante es que hemos aprendido a pensar como tráders analíticos. No nos hemos limitado a seguir a la multitud, sino que hemos buscado nuestro propio camino, nuestros propios patrones, nuestras propias oportunidades.

Recuerde que el mercado es producto de las acciones de personas vivas. El mercado crece y cambia, y nuestra misión es cambiar con él. Puede que los patrones de hoy no funcionen mañana, pero eso no es motivo para desesperarse: es una razón para aprender, adaptarse y crecer. Utilice este sistema como punto de partida. Experimente, mejore y cree el suyo propio. Tal vez sea usted quien encuentre el patrón que le abrirá la puerta al éxito en el trading.

¡Buena suerte en este apasionante viaje! Que sus patrones sean siempre rentables y sus pérdidas solo sean lecciones en el camino hacia el éxito. ¡Nos vemos en el mundo Fórex!

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

Archivos adjuntos |
PredictPattern.py (10.35 KB)
AutoPattern.mq5 (18.98 KB)
PatternEA.mq5 (16.14 KB)
PatternEAMult.mq5 (9.59 KB)
Elaboración de previsiones económicas: el potencial de Python Elaboración de previsiones económicas: el potencial de Python
¿Cómo utilizar los datos económicos del Banco Mundial para crear previsiones? ¿Qué ocurre si se combinan modelos de IA y economía?
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.
Reimaginando las estrategias clásicas (Parte VIII): Los mercados de divisas y metales preciosos en el USDCAD Reimaginando las estrategias clásicas (Parte VIII): Los mercados de divisas y metales preciosos en el USDCAD
En esta serie de artículos, revisamos estrategias de negociación bien conocidas para ver si podemos mejorarlas utilizando IA. En el artículo de hoy comprobaremos si existe una relación fiable entre los metales preciosos y las divisas.
Sistema de arbitraje de alta frecuencia en Python con MetaTrader 5 Sistema de arbitraje de alta frecuencia en Python con MetaTrader 5
Hoy vamos a crear un sistema de arbitraje legal a los ojos de los brókeres, que creará miles de precios sintéticos en el mercado Fórex, los analizará y negociará con éxito para obtener beneficios.