
Reimaginando estrategias clásicas en Python: Cruce de medias móviles (MAs, Moving Averages)
Introducción
Muchas de las estrategias de negociación actuales se concibieron en entornos de mercado muy diferentes. Evaluar su relevancia en los mercados contemporáneos dominados por algoritmos es crucial. Este artículo profundiza en la estrategia de cruce de medias móviles para evaluar su eficacia en el entorno financiero actual.
En este artículo se tratará lo siguiente:
- ¿Existen pruebas cuantitativas que respalden el uso continuado de la estrategia?
- ¿Qué ventajas ofrece la estrategia en comparación con el análisis directo de precios?
- ¿Sigue funcionando eficazmente la estrategia en medio de la negociación algorítmica moderna?
- ¿Existen otros indicadores que puedan mejorar la precisión de la estrategia?
- ¿Puede aprovecharse eficazmente la IA para predecir cruces de medias móviles antes de que se produzcan?
La técnica de emplear cruces de medias móviles se ha estudiado ampliamente durante décadas. El concepto fundamental de utilizar estas medias para detectar tendencias y señales de negociación ha sido un pilar del análisis técnico, aunque su origen exacto sigue siendo incierto.
La estrategia de cruce de medias móviles suele implicar dos medias móviles con periodos diferentes, pero la condición fundamental es que un periodo sea más largo que el otro. Cuando la media móvil de período más corto cruza por encima de la media móvil de período más largo, indica una posible tendencia alcista, y viceversa para una tendencia bajista.Los analistas técnicos han utilizado esta estrategia durante décadas para señalar puntos de entrada y salida, calibrar el sentimiento del mercado y para otras aplicaciones diversas. Para determinar su eficacia actual, someteremos la estrategia a una moderna prueba cuantitativa. Nuestro enfoque se detalla a continuación.
Fig. 1: Ejemplo de aplicación de cruces de medias móviles en el par CADJPY.
Descripción general
Estamos a punto de embarcarnos en un emocionante viaje en el que enlazaremos nuestro terminal MetaTrader 5 con nuestro entorno Python. En primer lugar, solicitaremos datos M15 para el par EURUSD desde el 1 de enero de 2020 hasta el 25 de junio de 2024. Este amplio conjunto de datos nos dará una visión global de los comportamientos recientes del mercado.
Nuestro próximo paso es establecer dos objetivos. El primero medirá nuestra precisión al predecir cambios directos de precios y nos servirá como base. Este punto de referencia nos ayudará a comparar qué tan bien nos desempeñamos al pronosticar cruces de promedios móviles. A lo largo del camino, buscaremos indicadores técnicos adicionales para mejorar nuestra precisión. Por último, pediremos a nuestros modelos informáticos que identifiquen las variables clave para predecir los cruces de medias móviles. Si el modelo no da prioridad a las dos medias móviles que utilizamos, podría indicar que nuestras suposiciones iniciales eran incorrectas.
Antes de entrar en cifras, consideremos los posibles resultados:
-
Superioridad de la predicción directa del precio: Si la predicción directa de los cambios de precio arroja una precisión superior o igual a la de los cruces de medias móviles, sugiere que la predicción de los cruces podría no proporcionar ninguna ventaja adicional, lo que cuestiona la validez de la estrategia.
-
Superioridad en la predicción de cruces: Si logramos una mayor precisión en la predicción de cruces de medias móviles, nos motivaría a buscar más datos para mejorar aún más nuestras predicciones, lo que pondría de relieve el valor potencial de la estrategia.
-
Irrelevancia de las medias móviles: Si nuestros modelos no identifican ninguna de las medias móviles como crucial para la predicción de cruces, implica que otras variables podrían ser más significativas, lo que sugiere que la supuesta relación entre las dos medias móviles no se mantiene.
-
Relevancia de las medias móviles: Si una o ambas medias móviles se marcan como importantes para predecir cruces, se confirma una relación sustancial entre ellas, lo que nos permite construir modelos fiables para realizar predicciones fundamentadas.
Este análisis nos ayudará a comprender los puntos fuertes y débiles de la utilización de cruces de medias móviles en nuestra estrategia de negociación, orientándonos hacia métodos de predicción más eficaces.
El experimento: ¿Siguen siendo fiables los cruces de medias móviles?
Empecemos por importar las bibliotecas estándar de Python que necesitamos.
import pandas as pd import pandas_ta as ta import numpy as np import MetaTrader5 as mt5 from datetime import datetime import seaborn as sns import time
A continuación, introducimos nuestros datos de acceso.
account = 123436536 password = "Enter Your Password" server = "Enter Your Broker"
A continuación, intentaremos acceder a nuestra cuenta de operaciones.
if(mt5.initialize(login=account,password=password,server=server)): print("Logged in succesfully") else: print("Failed to login")
Logged in successfully
A continuación definiremos algunas variables globales.
timeframe = mt5.TIMEFRAME_M15 deviation = 1000 volume = 0 lot_multiple = 10 symbol = "EURUSD"
A continuación, buscaremos los datos de mercado del símbolo que deseamos negociar.
#Setup trading volume symbols = mt5.symbols_get() for index,symbol in enumerate(symbols): if symbol.name == "EURUSD": print(f"{symbol.name} has minimum volume: {symbol.volume_min}") volume = symbol.volume_min * lot_multiple
EURUSD has minimum volume: 0.01
Ahora nos prepararemos para obtener los datos de entrenamiento.
#Specify date range of data to be modelled date_start = datetime(2020,1,1) date_end = datetime.now()
A continuación, definiremos la distancia a la que queremos realizar la predicción.
#Define how far ahead we are looking look_ahead = 20
A continuación, podemos proceder a obtener los datos del mercado de nuestro terminal MetaTrader 5, y luego etiquetar los datos. Nuestro esquema de etiquetado utiliza "1" para codificar un movimiento ascendente y un "0" para movimientos descendentes.
#Fetch market data market_data = pd.DataFrame(mt5.copy_rates_range("EURUSD",timeframe,date_start,date_end)) market_data["time"] = pd.to_datetime(market_data["time"],unit='s') #Add simple moving average technical indicator market_data.ta.sma(length=5,append=True) #Add simple moving average technical indicator market_data.ta.sma(length=50,append=True) #Delete missing rows market_data.dropna(inplace=True) #Add a column for the target market_data["target"] = 0 market_data["close_target"] = 0 #Encoding the target ma_cross_conditions = [ (market_data["SMA_5"].shift(-look_ahead) > market_data["SMA_50"].shift(-look_ahead)), (market_data["SMA_5"].shift(-look_ahead) < market_data["SMA_50"].shift(-look_ahead)) ] #Encoding pattern ma_cross_choices = [ #Fast MA above Slow MA 1, #Fast MA below Slow MA 0 ] price_conditions = [ (market_data["close"] > market_data["close"].shift(-look_ahead)), (market_data["close"] < market_data["close"].shift(-look_ahead)) ] #Encoding pattern price_choices = [ #Price fell 0, #Price rose 1 ] market_data["target"] = np.select(ma_cross_conditions,ma_cross_choices) market_data["close_target"] = np.select(price_conditions,price_choices) #The last rows do not have answers market_data = market_data[:-look_ahead] market_data
Fig. 2: Nuestro marco de datos con nuestros datos de mercado en su forma actual.
Ahora importaremos las bibliotecas de aprendizaje automático que necesitamos.
#XGBoost from xgboost import XGBClassifier #Catboost from catboost import CatBoostClassifier #Random forest from sklearn.ensemble import RandomForestClassifier #LDA and QDA from sklearn.discriminant_analysis import LinearDiscriminantAnalysis , QuadraticDiscriminantAnalysis #Logistic regression from sklearn.linear_model import LogisticRegression #Neural network from sklearn.neural_network import MLPClassifier #Time series split from sklearn.model_selection import TimeSeriesSplit #Accuracy metrics from sklearn.metrics import accuracy_score #Visualising performance from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt from sklearn.model_selection import learning_curve
Preparándose para realizar una división de series de tiempo en el conjunto de datos.
#Time series split splits = 10 gap = look_ahead models = ["Logistic Regression","Linear Discriminant Analysis","Quadratic Discriminant Analysis","Random Forest Classifier","XGB Classifier","Cat Boost Classifier","Neural Network Small","Neural Network Large"]
Evaluaremos la precisión de muchos modelos diferentes y almacenaremos la precisión obtenida de cada modelo en un marco de datos. Un marco de datos almacenará nuestra precisión al pronosticar cruces de promedios móviles, y el segundo marco de datos medirá nuestra precisión al pronosticar cambios en el precio directamente.
error_ma_crossover = pd.DataFrame(index=np.arange(0,splits),columns=models)
error_price = pd.DataFrame(index=np.arange(0,splits),columns=models)
Ahora procederemos a medir la precisión de cada modelo. Pero primero debemos definir las entradas que utilizarán nuestros modelos.
predictors = ["open","high","low","close","tick_volume","spread","SMA_5","SMA_50"]
Para medir la precisión de cada modelo, entrenaremos nuestros modelos en una fracción del conjunto de datos y luego los probaremos en el resto del conjunto de datos que no vio durante el entrenamiento. La biblioteca TimeSeriesSplit particiona nuestro marco de datos para nosotros y hace que este proceso sea más fácil.
tscv = TimeSeriesSplit(n_splits=splits,gap=gap)
#Training each model to predict changes in the moving average cross over for i,(train,test) in enumerate(tscv.split(market_data)): model = MLPClassifier(solver='lbfgs',alpha=1e-5,hidden_layer_sizes=(20, 10), random_state=1,early_stopping=True) model.fit( market_data.loc[train[0]:train[-1],predictors] , market_data.loc[train[0]:train[-1],"target"] ) error_ma_crossover.iloc[i,7] = accuracy_score(market_data.loc[test[0]:test[-1],"target"],model.predict(market_data.loc[test[0]:test[-1],predictors]))
#Training each model to predict changes in the close price for i,(train,test) in enumerate(tscv.split(market_data)): model = MLPClassifier(solver='lbfgs',alpha=1e-5,hidden_layer_sizes=(20, 10), random_state=1,early_stopping=True) model.fit( market_data.loc[train[0]:train[-1],predictors] , market_data.loc[train[0]:train[-1],"close_target"] ) error_price.iloc[i,7] = accuracy_score(market_data.loc[test[0]:test[-1],"close_target"],model.predict(market_data.loc[test[0]:test[-1],predictors]))
Veamos primero el marco de datos que mide nuestra precisión al pronosticar cambios en los precios directamente.
error_price
Fig. 3: Nuestra precisión a la hora de predecir directamente las variaciones de los precios.
Interpretemos los resultados antes de continuar. El primer comentario que podemos hacer es que ninguno de los modelos que tenemos está rindiendo bien en la tarea, algunos modelos demostraron menos del 50% de precisión cuando predicen el precio directamente. Este rendimiento es desalentador, lo que implica que podríamos haber rendido más o menos a la par que estos modelos simplemente adivinando al azar. Nuestros modelos están dispuestos en orden de complejidad creciente, con la regresión logística simple a la izquierda y las redes neuronales profundas a la derecha. Como podemos observar, el aumento de la complejidad de los modelos no aumentó nuestra precisión a la hora de predecir el precio directamente. Veamos ahora si, en cambio, hay alguna mejora cuando se predicen cruces de medias móviles.
error_ma_crossover
Fig. 4: Nuestra precisión en la predicción de cruces de medias móviles.
Como podemos ver en el marco de datos anterior, el análisis discriminante lineal (LDA, Linear Discriminant Analysis) funcionó extremadamente bien en esta tarea. Fue el modelo con mejor rendimiento que examinamos por un amplio margen. Además, cuando se compara el desempeño mejorado del modelo LDA con el pobre desempeño de la primera tarea, podemos ver claramente que los cruces de promedios móviles pueden ser más confiables para pronosticar que los cambios directos en el precio. En tal caso, no se pueden discutir los beneficios de pronosticar cruces de medias móviles.
Visualizando los resultados
Visualicemos los resultados obtenidos anteriormente.
Fig. 5: Visualización de los resultados obtenidos.
La mejora del algoritmo LDA es notablemente visible en los gráficos de caja, lo que indica un aprendizaje significativo por parte de nuestro modelo. Además, hubo una mejora leve pero notable en el rendimiento de la regresión logística. En particular, LDA produjo consistentemente puntuaciones estrechamente agrupadas en diagramas de caja al pronosticar cruces de promedios móviles, lo que demuestra una precisión y consistencia deseables. Esta agrupación sugiere que las predicciones del modelo eran estables, con probables residuos estacionarios que indicaban una relación confiable aprendida por el modelo.
Ahora, analicemos los tipos de errores que cometió nuestro modelo. Nuestro objetivo es determinar si funciona mejor al identificar movimientos ascendentes, descendentes, o si su rendimiento está equilibrado en ambas tareas.
Fig. 6: Una matriz de confusión del rendimiento de nuestro modelo LDA.
La matriz de confusión de arriba muestra la clasificación real a la izquierda y la predicción de nuestro modelo en la parte inferior. Los datos nos indican que nuestro modelo cometió más errores al predecir movimientos ascendentes: clasificó erróneamente un movimiento ascendente como descendente el 47% de las veces. Por otro lado, nuestro modelo funcionó muy bien al predecir movimientos a la baja; solo confundió un verdadero movimiento a la baja con un movimiento al alza el 25% de las veces. Por lo tanto, podemos ver claramente que nuestro modelo es mejor para predecir movimientos descendentes que para predecir movimientos ascendentes.
Podemos visualizar el progreso de aprendizaje de nuestro modelo a medida que encuentra cantidades crecientes de datos de entrenamiento. El gráfico a continuación sirve para evaluar si nuestro modelo está sobreajustado o subajustado a los datos de entrenamiento. El sobreajuste ocurre cuando el modelo aprende ruido de los datos y no logra capturar relaciones significativas. Por otro lado, el subajuste se indica mediante una brecha significativa entre la precisión del entrenamiento (representada por la línea azul) y la precisión de la validación (la línea naranja) en el gráfico. En nuestro gráfico actual, observamos una brecha notable pero no extensa entre los puntajes de entrenamiento y validación, lo que sugiere que nuestro modelo LDA de hecho está sobreajustando los datos de entrenamiento. Sin embargo, la escala del lado izquierdo indica que este sobreajuste no es grave.
Fig. 7: La curva de aprendizaje de nuestro clasificador LDA.
Por otro lado, el subajuste se caracteriza por una baja precisión de entrenamiento y validación. Como ejemplo, hemos incluido la curva de aprendizaje de uno de nuestros modelos de bajo rendimiento: la red neuronal pequeña. En el gráfico siguiente, observamos una relación inestable entre el rendimiento de nuestro modelo y la cantidad de datos de entrenamiento a los que ha estado expuesto. Inicialmente, el rendimiento de validación del modelo se deteriora a medida que aumentan los datos, hasta que alcanza un punto de inflexión y comienza a mejorar a medida que el tamaño del entrenamiento se acerca a las 10.000 muestras. Posteriormente, la mejora se estanca, con solo mejoras marginales a pesar de los grandes aumentos continuos en la cantidad de datos de entrenamiento disponibles.
Fig. 8: La curva de aprendizaje de nuestra pequeña red neuronal.
Eliminación de funciones
En la mayoría de los proyectos de aprendizaje automático, es poco común que todas las entradas se relacionen directamente con la variable de destino. Normalmente, solo un subconjunto de las entradas disponibles son relevantes para predecir el objetivo. La eliminación de entradas irrelevantes ofrece varias ventajas, tales como:
- Eficiencia computacional mejorada durante el entrenamiento de modelos y la ingeniería de características.
- Mayor precisión del modelo, sobre todo si las características eliminadas eran ruidosas.
A continuación, debemos determinar si existe una relación significativa entre los promedios móviles. Emplearemos algoritmos de eliminación de características para validar la relación asumida. Si estos algoritmos no logran eliminar los promedios móviles de la lista de entrada, indica que existe una relación significativa. Por el contrario, si eliminan con éxito estas características, esto sugiere que no existe una relación significativa entre los promedios móviles y el cruce de promedios móviles.
Emplearemos una técnica de selección de características conocida como selección hacia atrás. Este método comienza ajustando un modelo lineal utilizando todas las entradas disponibles y luego midiendo la precisión del modelo. Posteriormente, se elimina una característica a la vez y se anota el impacto en la precisión del modelo. La característica que causa la menor disminución en la precisión se elimina en cada paso hasta que no queden características. En esta etapa, el algoritmo selecciona automáticamente las características más importantes que ha identificado y las recomienda para su uso.
Un inconveniente importante de la eliminación de características que vale la pena mencionar es que cuando hay columnas ruidosas y sin importancia en nuestro conjunto de datos, las columnas importantes pueden parecer poco informativas. En consecuencia, el algoritmo de selección hacia atrás podría eliminar inadvertidamente una característica importante porque parece poco informativa debido al ruido en el sistema.
Procedamos ahora a ver qué columnas considera nuestra computadora que son importantes. Comenzamos importando una biblioteca llamada mlxtend que contiene implementaciones del algoritmo de selección hacia atrás.
from mlxtend.feature_selection import SequentialFeatureSelector
Luego aplicamos el algoritmo en nuestro conjunto de datos. Prestemos especial atención a 3 de los parámetros que pasamos:
- "k_features=" instruye al algoritmo sobre cuántas columnas seleccionar. Podemos indicarle al algoritmo que seleccione solo las columnas que considere necesarias pasando un intervalo que comience desde 1 hasta el número total de columnas en el conjunto de datos.
- "forward=" le indica al algoritmo si debe utilizar selección hacia adelante o hacia atrás; queremos utilizar selección hacia atrás, por lo tanto, establecemos este parámetro en "False".
- "n_jobs=" le indica al algoritmo si debe realizar cálculos en paralelo, pasamos "-1" para darle permiso al algoritmo para usar todos los núcleos disponibles, esto reducirá significativamente la cantidad de tiempo empleado.
backward_feature_selector = SequentialFeatureSelector(LinearDiscriminantAnalysis(), k_features=(1,market_data.loc[:,predictors].shape[1]), forward=False, verbose=2, scoring="accuracy", cv=5, n_jobs=-1 ).fit(market_data.loc[:,predictors],market_data.loc[:,"target"])
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
[Parallel(n_jobs=-1)]: Done 3 out of 8 | elapsed: 8.0s remaining: 13.3s
[Parallel(n_jobs=-1)]: Done 8 out of 8 | elapsed: 8.0s remaining: 0.0s
[Parallel(n_jobs=-1)]: Done 8 out of 8 | elapsed: 8.0s finished
Una vez realizado el proceso, podemos obtener una lista de las entradas que nuestro algoritmo considera importantes utilizando el siguiente comando.
backward_feature_selector.k_feature_names_
('open', 'high', 'close', 'SMA_5', 'SMA_50')
Y como podemos ver, el algoritmo de selección hacia atrás incluyó nuestros 2 promedios móviles en su lista de características importantes. Esta es una gran noticia para nosotros porque valida que nuestra estrategia comercial no es simplemente el resultado de una regresión espuria.
Ingeniería de funciones
Ahora que hemos establecido una relación significativa entre nuestros dos promedios móviles que justifica mayores esfuerzos de mejora, exploremos si indicadores técnicos adicionales pueden mejorar nuestra precisión al pronosticar cruces de promedios móviles. Aquí es donde el aprendizaje automático se inclina más hacia el arte que hacia la ciencia, ya que predecir de antemano qué insumos serán beneficiosos es un desafío. Nuestro enfoque implicará agregar varias características que creemos que podrían ser útiles y evaluar su impacto real.
Recopilaremos datos de mercado del mismo mercado que antes, pero esta vez incorporaremos indicadores adicionales:
- Moving Average Convergence Divergence (MACD): El MACD es un indicador técnico de confirmación de tendencia muy potente que puede ayudarnos a observar mejor los cambios en los regímenes subyacentes del mercado.
- Awesome Oscillator: El oscilador Awesome es famoso por proporcionar señales de salida muy fiables, y puede mostrarnos claramente cuándo una tendencia cambia de impulso.
- Aroon: El indicador Aroon se utiliza para identificar el inicio de nuevas tendencias.
- Chaikins Commodity Index: El índice Chaikins de materias primas actúa como barómetro para medir si un valor financiero está sobrecomprado o sobrevendido.
- Percent Return: El indicador de rentabilidad porcentual nos ayuda a observar el crecimiento de la cotización y si éste es positivo o negativo.
#Fetch market data market_data = pd.DataFrame(mt5.copy_rates_range("EURUSD",timeframe,date_start,date_end)) market_data["time"] = pd.to_datetime(market_data["time"],unit='s') #Add simple moving average technical indicator market_data.ta.sma(length=5,append=True) #Add simple moving average technical indicator market_data.ta.sma(length=50,append=True) #Add macd market_data.ta.macd(append=True) #Add awesome oscilator market_data.ta.ao(append=True) #Add aroon market_data.ta.aroon(append=True) #Add chaikins comodity index market_data.ta.cci(append=True) #Add percent return market_data.ta.percent_return(append=True) #Delete missing rows market_data.dropna(inplace=True) #Add the target market_data["target"] = 0 market_data.loc[market_data["SMA_5"].shift(-look_ahead) > market_data["SMA_50"].shift(-look_ahead),"target"] = 1 market_data.loc[market_data["SMA_5"].shift(-look_ahead) < market_data["SMA_50"].shift(-look_ahead),"target"] = 0 #The last rows do not have answers market_data = market_data[:-look_ahead] market_data
Fig. 9: Algunas de las nuevas filas adicionales que hemos añadido a nuestro marco de datos.
Tras realizar la selección de características, nuestro algoritmo de selección hacia atrás identificó las siguientes variables como importantes.
backward_feature_selector = SequentialFeatureSelector(LinearDiscriminantAnalysis(), k_features=(1,market_data.loc[:,predictors].shape[1]), forward=False, verbose=2, scoring="accuracy", cv=5 ).fit(market_data.iloc[:,1:-1],market_data.loc[:,"target"])
backward_feature_selector.k_feature_names_
('close', 'tick_volume', 'spread', 'SMA_5', 'SMA_50', 'MACDh_12_26_9', 'AO_5_34')
Desarrollando nuestra estrategia comercial
Ahora estamos listos para poner todo lo que hemos aprendido hasta ahora en una estrategia comercial consolidada.
Primero comenzamos ajustando nuestro modelo a todos los datos de entrenamiento que tenemos disponibles, utilizando solo las columnas que hemos identificado como útiles.
predictors = ['close','tick_volume','spread','SMA_5','SMA_50','MACDh_12_26_9','AO_5_34'] model = LinearDiscriminantAnalysis() model.fit(market_data.loc[:,predictors],market_data.loc[:,"target"])
A continuación definimos funciones para obtener datos del mercado desde nuestro terminal MetaTrader 5.
def get_prices(): start = datetime(2024,6,1) end = datetime.now() data = pd.DataFrame(mt5.copy_rates_range("EURUSD",timeframe,start,end)) #Add simple moving average technical indicator data.ta.sma(length=5,append=True) data.ta.sma(length=50,append=True) #Add awesome oscilator data.ta.ao(append=True) #Add macd data.ta.macd(append=True) #Delete missing rows data.dropna(inplace=True) data['time'] = pd.to_datetime(data['time'],unit='s') data.set_index('time',inplace=True) data = data.loc[:,['close','tick_volume','spread','SMA_5','SMA_50','MACDh_12_26_9','AO_5_34']] data = data.iloc[-2:,:] return(data)
Posteriormente necesitamos otro método para obtener predicciones de nuestro modelo LDA.
#Get signals LDA model def ai_signal(input_data,_model): #Get a forecast forecast = _model.predict(input_data) return forecast[1]
Ahora podemos construir nuestra estrategia comercial.
#Now we define the main body of our Python Moving Average Crossover Trading Bot if __name__ == '__main__': #We'll use an infinite loop to keep the program running while True: #Fetching model prediction signal = ai_signal(get_prices(),model) #Decoding model prediction into an action if signal == 1: direction = 'buy' elif signal == 0: direction = 'sell' print(f'AI Forecast: {direction}') #Opening A Buy Trade #But first we need to ensure there are no opposite trades open on the same symbol if direction == 'buy': #Close any sell positions for pos in mt5.positions_get(): if pos.type == 1: #This is an open sell order, and we need to close it close_order(pos.ticket) if not mt5.positions_totoal(): #We have no open positions mt5.Buy(symbol,volume) #Opening A Sell Trade elif direction == 'sell': #Close any buy positions for pos in mt5.positions_get(): if pos.type == 0: #This is an open buy order, and we need to close it close_order(pos.ticket) if not mt5.positions_get(): #We have no open positions mt5.sell(symbol,volume) print('time: ', datetime.now()) print('-------\n') time.sleep(60)
AI Forecast: sell
time: 2024-06-25 14:35:37.954923
-------
Fig. 10: Nuestra estrategia comercial en acción.
Implementación en MQL5
De ahora en adelante, utilizaremos la API MQL5 para desarrollar nuestro propio clasificador desde cero. Existen numerosas ventajas al crear un clasificador personalizado en MQL5. Como autor, creo firmemente que las soluciones MQL5 nativas ofrecen una flexibilidad incomparable.
Si tuviéramos que exportar nuestro modelo al formato ONNX, necesitaríamos un modelo separado para cada mercado en el que deseamos operar. Además, operar en diferentes marcos temporales requeriría múltiples modelos ONNX para cada mercado. Al construir nuestro clasificador directamente en MQL5, obtenemos la capacidad de operar en cualquier mercado sin estas limitaciones.
Así que vamos a crear un nuevo proyecto.
Fig. 11: Creación de un EA para implementar nuestra estrategia.
Nuestra primera tarea es definir algunas variables globales que utilizaremos a lo largo de nuestro programa.
//Global variables int ma_5,ma_50; double bid, ask; double min_volume; double ma_50_reading[],ma_5_reading[]; int size; double current_prediction; int state = -1; matrix ohlc; vector target; double b_nort = 0; double b_one = 0; double b_two = 0; long min_distance,atr_stop;
También tendremos entradas que el usuario final podrá ajustar.
//Inputs int input lot_multiple = 20; int input positions = 2; double input sl_width = 0.4;
Por último, importaremos la biblioteca comercial para ayudarnos a administrar nuestras posiciones.
//Libraries #include <Trade\Trade.mqh> CTrade Trade;
Continuando, necesitamos definir funciones auxiliares que nos ayudarán a obtener datos, etiquetar los datos de entrenamiento, entrenar nuestro modelo y obtener predicciones de nuestro modelo. Comencemos por definir una función para obtener datos de entrenamiento y etiquetar el objetivo para nuestro clasificador.
//+----------------------------------------------------------------------+ //|This function is responsible for getting our training data ready | //+----------------------------------------------------------------------+ void get_training_data(void) { //How much data are we going to use? size = 100; //Copy price data ohlc.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,1,size); //Get indicator data ma_50 = iMA(_Symbol,PERIOD_CURRENT,50,0,MODE_EMA,PRICE_CLOSE); ma_5 = iMA(_Symbol,PERIOD_CURRENT,5,0,MODE_EMA,PRICE_CLOSE); CopyBuffer(ma_50,0,0,size,ma_50_reading); CopyBuffer(ma_5,0,0,size,ma_5_reading); ArraySetAsSeries(ma_50_reading,true); ArraySetAsSeries(ma_5_reading,true); //Label the target target = vector::Zeros(size); for(int i = 0; i < size; i++) { if(ma_5_reading[i] > ma_50_reading[i]) { target[i] = 1; } else if(ma_5_reading[i] < ma_50_reading[i]) { target[i] = 0; } } //Feedback Print("Done getting training data."); }
Nuestro modelo tiene tres coeficientes que utiliza para hacer predicciones. Estos coeficientes necesitan ser optimizados. Utilizaremos una ecuación de actualización fácil de usar para principiantes para ajustar estos coeficientes. Al medir el error en las predicciones de nuestro modelo, modificaremos iterativamente los coeficientes para minimizar el error y mejorar la precisión de nuestro sistema. Pero antes de que podamos comenzar a optimizar el modelo, primero debemos definir cómo nuestro modelo hace predicciones.
//+----------------------------------------------------------------------+ //|This function is responsible for making predictions using our model | //+----------------------------------------------------------------------+ double model_predict(double input_one,double input_two) { //We simply return the probability that the shorter moving average will rise above the slower moving average double prediction = 1 / (1 + MathExp(-(b_nort + (b_one * input_one) + (b_two * input_two)))); return prediction; }
Ahora que nuestro modelo puede hacer predicciones, podemos medir el error en sus predicciones y comenzar el proceso de optimización. Inicialmente, los tres coeficientes se establecerán en 0. Luego ajustaremos iterativamente los coeficientes en pequeños pasos para minimizar el error total en nuestro sistema.
//+----------------------------------------------------------------------+ //|This function is responsible for training our model | //+----------------------------------------------------------------------+ bool train_model(void) { //Update the coefficients double learning_rate = 0.3; for(int i = 0; i < size; i++) { //Get a prediction from the model current_prediction = model_predict(ma_5_reading[i],ma_50_reading[i]); //Update each coefficient b_nort = b_nort + learning_rate * (target[i] - current_prediction) * current_prediction * (1 - current_prediction) * 1; b_one = b_one + learning_rate * (target[i] - current_prediction) * current_prediction * (1-current_prediction) * ma_5_reading[i]; b_two = b_two + learning_rate * (target[i] - current_prediction) * current_prediction * (1-current_prediction) * ma_50_reading[i]; Print(current_prediction); } //Show updated coefficient values Print("Updated coefficient values"); Print(b_nort); Print(b_one); Print(b_two); return(true); }
Después de entrenar con éxito el modelo, sería beneficioso tener una función que recupere predicciones de nuestro modelo. Estas predicciones nos servirán como señales comerciales. Recuerde que una predicción de 1 es una señal de compra, lo que indica que nuestro modelo espera que el promedio móvil más corto aumente por encima del promedio móvil de período más largo. Por el contrario, una predicción de 0 es una señal de venta, lo que indica que nuestro modelo espera que el promedio móvil más corto caiga por debajo del promedio móvil más largo.
//Get the model's current forecast void current_forecast() { //Get indicator data ma_50 = iMA(_Symbol,PERIOD_CURRENT,50,0,MODE_EMA,PRICE_CLOSE); ma_5 = iMA(_Symbol,PERIOD_CURRENT,5,0,MODE_EMA,PRICE_CLOSE); CopyBuffer(ma_50,0,0,1,ma_50_reading); CopyBuffer(ma_5,0,0,1,ma_5_reading); //Get model forecast model_predict(ma_5_reading[0],ma_50_reading[0]); interpret_forecast(); }
Queremos que nuestro asesor experto actúe según las predicciones del modelo. Por lo tanto, escribiremos una función para interpretar el pronóstico del modelo y tomar la acción apropiada: comprar cuando el modelo predice 1 y vender cuando el modelo predice 0.
//+----------------------------------------------------------------------+ //|This function is responsible for taking action on our model's forecast| //+----------------------------------------------------------------------+ void interpret_forecast(void) { if(current_prediction > 0.5) { state = 1; Trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,min_volume * lot_multiple,ask,0,0,"Volatitlity Doctor AI"); } if(current_prediction < 0.5) { state = 0; Trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,min_volume * lot_multiple,bid,0,0,"Volatitlity Doctor AI"); } }
Ahora que nuestra aplicación puede aprender de los datos, hacer predicciones y actuar en consecuencia, necesitamos crear funciones adicionales para administrar cualquier posición abierta. En concreto, queremos que nuestro programa añada trailing stop loss y take profit a cada posición para gestionar nuestros niveles de riesgo. No queremos tener posiciones abiertas sin un límite de riesgo definido. La mayoría de las estrategias comerciales recomiendan un tamaño de stop loss fijo de 100 pips, pero queremos asegurarnos de que nuestros niveles de stop loss y take profit se coloquen dinámicamente en función de la volatilidad actual del mercado. Por lo tanto, utilizaremos el rango verdadero promedio (ATR) para calcular qué tan amplios o estrechos deben ser nuestros stops. Utilizaremos un múltiplo del ATR para determinar estos niveles.
//+----------------------------------------------------------------------+ //|This function is responsible for calculating our SL & TP values | //+----------------------------------------------------------------------+ void CheckAtrStop() { //First we iterate over the total number of open positions for(int i = PositionsTotal() -1; i >= 0; i--) { //Then we fetch the name of the symbol of the open position string symbol = PositionGetSymbol(i); //Before going any furhter we need to ensure that the symbol of the position matches the symbol we're trading if(_Symbol == symbol) { //Now we get information about the position ulong ticket = PositionGetInteger(POSITION_TICKET); //Position Ticket double position_price = PositionGetDouble(POSITION_PRICE_OPEN); //Position Open Price long type = PositionGetInteger(POSITION_TYPE); //Position Type double current_stop_loss = PositionGetDouble(POSITION_SL); //Current Stop loss value //If the position is a buy if(type == POSITION_TYPE_BUY) { //The new stop loss value is just the ask price minus the ATR stop we calculated above double atr_stop_loss = NormalizeDouble(ask - ((min_distance * sl_width)/2),_Digits); //The new take profit is just the ask price plus the ATR stop we calculated above double atr_take_profit = NormalizeDouble(ask + (min_distance * sl_width),_Digits); //If our current stop loss is less than our calculated ATR stop loss //Or if our current stop loss is 0 then we will modify the stop loss and take profit if((current_stop_loss < atr_stop_loss) || (current_stop_loss == 0)) { Trade.PositionModify(ticket,atr_stop_loss,atr_take_profit); } } //If the position is a sell else if(type == POSITION_TYPE_SELL) { //The new stop loss value is just the ask price minus the ATR stop we calculated above double atr_stop_loss = NormalizeDouble(bid + ((min_distance * sl_width)/2),_Digits); //The new take profit is just the ask price plus the ATR stop we calculated above double atr_take_profit = NormalizeDouble(bid - (min_distance * sl_width),_Digits); //If our current stop loss is greater than our calculated ATR stop loss //Or if our current stop loss is 0 then we will modify the stop loss and take profit if((current_stop_loss > atr_stop_loss) || (current_stop_loss == 0)) { Trade.PositionModify(ticket,atr_stop_loss,atr_take_profit); } } } } }
Luego necesitamos una función que llamaremos siempre que queramos calcular nuevos valores de Stop Loss y Take Profit.
//+------------------------------------------------------------------+ //|This function is responsible for updating our SL&TP values | //+------------------------------------------------------------------+ void ManageTrade() { CheckAtrStop(); }
Ahora que hemos definido nuestras funciones auxiliares, podemos comenzar a llamarlas dentro de nuestros controladores de eventos. Cuando nuestro programa se carga por primera vez, queremos iniciar el proceso de entrenamiento. Por lo tanto, llamaremos a nuestra función auxiliar responsable de entrenar a nuestro experto dentro del controlador de eventos OnInit.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //Define important global variables min_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); min_distance = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL); //Train the model get_training_data(); if(train_model()) { interpret_forecast(); } return(INIT_SUCCEEDED); }
Después de entrenar el modelo, podemos comenzar a operar realmente.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //Get updates bid and ask prices bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); if(PositionsTotal() == 0) { current_forecast(); } if(PositionsTotal() > 0) { ManageTrade(); } }
Fig. 12: Una muestra de la salida de nuestro asesor experto.
Fig. 13: Nuestro asesor experto en acción.
Conclusión
En este artículo, hemos demostrado que es computacionalmente más fácil para nuestro modelo predecir cruces de medias móviles que predecir cambios en el precio directamente.
Como en todos mis artículos, prefiero proporcionar explicaciones técnicas al final, demostrando primero el principio. Hay varias razones posibles para esta observación. Una posible razón es que, dependiendo de los períodos elegidos, los promedios móviles pueden no cruzarse con tanta frecuencia ya que los precios cambian de dirección de manera errática. En otras palabras, durante las últimas dos horas, el precio puede haber subido, luego bajado o cambiado de dirección dos veces. Sin embargo, durante ese mismo período, es posible que los promedios móviles no se hayan cruzado en absoluto. Por lo tanto, los cruces de medias móviles pueden ser más fáciles de pronosticar porque no cambian de dirección tan rápidamente como lo hace el propio precio. Esta es sólo una posible explicación. Siéntete libre de pensar por ti mismo, sacar tus propias conclusiones y compartirlas en los comentarios a continuación.
De aquí en adelante, empleamos la selección hacia atrás para la eliminación de características, una técnica en la que un modelo lineal se entrena iterativamente con una característica eliminada en cada paso en función de su impacto en la precisión del modelo. Este enfoque ayuda a identificar y retener las características más informativas, aunque es susceptible de eliminar características importantes que pueden parecer poco informativas debido al ruido.
Después de validar una relación significativa entre dos promedios móviles, exploramos la integración de indicadores técnicos adicionales: MACD, Awesome Oscillator, Aroon, Chaikins Commodity Index y Percent Return. Estos indicadores tienen como objetivo mejorar nuestra capacidad de pronosticar con precisión los cruces de medias móviles. Sin embargo, la selección de estos indicadores sigue siendo en cierta medida un arte debido a la naturaleza impredecible de su impacto en el rendimiento del modelo.
En general, nuestro enfoque combina la validación empírica con la selección de características estratégicas para demostrar cuantitativamente que, de hecho, los cruces de promedios móviles se pueden predecir y, además, cualquier esfuerzo invertido en tratar de mejorar esta estrategia comercial no sería enfáticamente una pérdida de tiempo.
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/15160





- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso