
Análisis de múltiples símbolos con Python y MQL5 (Parte I): Fabricantes de circuitos integrados del NASDAQ
Hay muchas formas en las que un inversor puede diversificar su cartera. Además, hay muchas métricas diferentes que pueden utilizarse como criterio para determinar qué tan bien se ha optimizado la cartera. Es poco probable que un solo inversor disponga de tiempo o recursos suficientes para considerar detenidamente todas sus opciones antes de comprometerse con una decisión tan importante. En esta serie de artículos, lo guiaremos a través de la gran variedad de opciones que tiene por delante en su viaje para operar con múltiples símbolos simultáneamente. Nuestro objetivo es ayudarle a decidir qué estrategias mantener y cuáles pueden no ser adecuadas para usted.
Visión general de la estrategia de negociación
En este debate, hemos seleccionado una cesta de valores que están fundamentalmente relacionados entre sí. Hemos seleccionado 5 valores de empresas que diseñan y venden circuitos integrados en su ciclo empresarial. Estas empresas son Broadcom, Cisco, Intel, NVIDIA y Comcast. Las 5 empresas están cotizadas en el mercado NASDAQ (National Association of Securities Dealers Automated Quotations). NASDAQ se fundó en 1971 y es la mayor bolsa de Estados Unidos por volumen de negociación..
Los circuitos integrados se han convertido en un elemento básico de nuestra vida cotidiana. Estos chips electrónicos impregnan todos los aspectos de nuestra vida moderna, desde los servidores propietarios de MetaQuotes que alojan este mismo sitio web en el que está leyendo este artículo, hasta el dispositivo que está utilizando para leer este artículo, todos estos dispositivos se basan en tecnología que muy probablemente ha sido desarrollada por una de estas 5 empresas. El primer circuito integrado del mundo fue desarrollado por Intel, con la marca Intel 4004, y se lanzó al mercado en 1971, el mismo año en que se fundó el mercado NASDAQ. El Intel 4004 tenía aproximadamente 2.600 transistores, muy lejos de los chips modernos, que cuentan con miles de millones de transistores.
Motivados por la demanda mundial de circuitos integrados, deseamos exponernos inteligentemente al mercado de los chips. Dada una cesta de estos 5 valores, demostraremos cómo maximizar la rentabilidad de su cartera distribuyendo prudentemente el capital entre ellos. Un enfoque tradicional de distribución uniforme del capital entre las 5 acciones no bastará en los mercados modernos y volátiles. En su lugar, construiremos un modelo que nos informe de si debemos comprar o vender cada acción, y de las cantidades óptimas que debemos negociar. En otras palabras, estamos utilizando los datos que tenemos a mano para aprender algorítmicamente el tamaño y las cantidades de nuestras posiciones.
Resumen de la metodología
Comenzamos obteniendo 100000 filas de datos de mercado M1 para cada uno de los 5 valores de nuestra cesta, desde nuestro terminal MetaTrader 5 utilizando la librería Python de MetaTrader 5. Tras convertir los datos de precios ordinarios en variaciones porcentuales, realizamos un análisis exploratorio de los datos de rentabilidad del mercado.
Observamos débiles niveles de correlación entre los 5 valores. Además, nuestros diagramas de caja mostraron claramente que el rendimiento promedio de cada acción fue cercano a 0. También graficamos los rendimientos de cada acción de forma superpuesta y pudimos observar claramente que los rendimientos de las acciones de NVIDIA parecían ser los más volátiles. Por último, creamos gráficos de pares entre las cinco acciones que seleccionamos y, desafortunadamente, no pudimos observar ninguna relación discernible que pudiéramos aprovechar.
A partir de ahí, utilizamos la biblioteca SciPy para encontrar pesos óptimos para cada una de nuestras 5 acciones en nuestra cartera. Permitiremos que los 5 pesos oscilen entre -1 y 1. Siempre que el peso de nuestra cartera está por debajo de 0, el algoritmo nos dice que vendamos y, a la inversa, cuando nuestros pesos están por encima de 0, los datos nos sugieren que compremos.
Después de calcular los pesos óptimos de la cartera, integramos estos datos en nuestra aplicación comercial para garantizar que siempre mantuviera un número óptimo de posiciones abiertas en cada mercado. Nuestra aplicación comercial está diseñada para cerrar automáticamente cualquier posición abierta si alcanza un nivel de ganancia especificado por el usuario final.
Obteniendo los datos
Para comenzar, primero importemos las bibliotecas que necesitamos.
#Import the libraries we need import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt import MetaTrader5 as mt5 from scipy.optimize import minimize
Ahora inicialicemos el terminal MetaTrader 5.
#Initialize the terminal
mt5.initialize()
Definir la cesta de valores que deseamos negociar.
#Now let us fetch the data we need on chip manufacturing stocks #Broadcom, Cisco, Comcast, Intel, NVIDIA stocks = ["AVGO.NAS","CSCO.NAS","CMCSA.NAS","INTC.NAS","NVDA.NAS"]
Creemos un marco de datos para almacenar nuestros datos de mercado.
#Let us create a data frame to store our stock returns amount = 100000 returns = pd.DataFrame(columns=stocks,index=np.arange(0,amount))
Ahora buscaremos nuestros datos de mercado.
#Fetch the stock returns for stock in stocks: temp = pd.DataFrame(mt5.copy_rates_from_pos(stock,mt5.TIMEFRAME_M1,0,amount)) returns[[stock]] = temp[["close"]].pct_change()
Formateemos nuestros datos.
#Format the data set
returns.dropna(inplace=True)
returns.reset_index(inplace=True,drop=True)
returns
Por último, multiplica los datos por 100 para guardarlos como porcentajes.
#Convert the returns to percentages returns = returns * 100 returns
Análisis exploratorio de datos
A veces, podemos ver visualmente la relación entre las variables del sistema. Analicemos los niveles de correlación en nuestros datos para ver si hay combinaciones lineales que podamos aprovechar. Desafortunadamente, nuestros niveles de correlación no son impresionantes y hasta el momento no parece haber dependencias lineales que podamos explotar.
#Let's analyze if there is any correlation in the data sns.heatmap(returns.corr(),annot=True)
Figura 1: Nuestro mapa de calor de correlación.
Analicemos diagramas de dispersión por pares de nuestros datos. Cuando trabajamos con grandes conjuntos de datos, es fácil que se nos escapen relaciones no triviales. Los gráficos por pares minimizarán las posibilidades de que esto suceda. Desafortunadamente, no hubo relaciones fácilmente observables en los datos que nos revelaron nuestros gráficos.
#Let's create pair plots of our data sns.pairplot(returns)
Figura 2: Algunos de nuestros diagramas de dispersión por pares.
Al graficar los rendimientos que observamos en los datos, vemos que NVIDIA parece tener los rendimientos más volátiles.
#Lets also visualize our returns
returns.plot()
Figura 3: Trazado de nuestros rendimientos del mercado.
Visualizar nuestros rendimientos de mercado como diagramas de caja nos muestra claramente que el rendimiento promedio del mercado es 0.
#Let's try creating box-plots sns.boxplot(returns)
Figura 4: Visualización de nuestros rendimientos de mercado como diagramas de caja.
Optimización de cartera
Ahora estamos listos para comenzar a calcular los pesos óptimos de la asignación de capital para cada acción. Inicialmente, asignaremos nuestros pesos aleatoriamente. Además, también crearemos una estructura de datos para almacenar el progreso de nuestro algoritmo de optimización.
#Define random weights that add up to 1 weights = np.array([1,0.5,0,0.5,-1]) #Create a data structure to store the progress of the algorithm evaluation_history = []
La función objetivo de nuestro procedimiento de optimización será el rendimiento de nuestra cartera bajo los pesos dados. Tenga en cuenta que los rendimientos de nuestra cartera se calcularán utilizando la media geométrica de los rendimientos de los activos. Elegimos emplear la media geométrica en lugar de la media aritmética porque, cuando se trata de valores positivos y negativos, calcular la media ya no es una tarea trivial. Si hubiéramos abordado este problema de manera casual y hubiéramos empleado la media aritmética, podríamos haber calculado fácilmente una rentabilidad de cartera de 0. Podemos utilizar algoritmos de minimización para problemas de maximización multiplicando el rendimiento de la cartera por menos 1 antes de devolverlo al algoritmo de optimización.
#Let us now get ready to maximize our returns #First we need to define the cost function def cost_function(x): #First we need to calculate the portfolio returns with the suggested weights portfolio_returns = np.dot(returns,x) geom_mean = ((np.prod( 1 + portfolio_returns ) ** (1.0/99999.0)) - 1) #Let's keep track of how our algorithm is performing evaluation_history.append(-geom_mean) return(-geom_mean)
Definamos ahora la restricción que garantiza que todos nuestros pesos sumen 1. Tenga en cuenta que solo unos pocos procedimientos de optimización en SciPy admiten restricciones de igualdad. Las restricciones de igualdad informan al módulo SciPy que nos gustaría que esta función sea igual a 0. Por lo tanto, queremos que la diferencia entre el valor absoluto de nuestros pesos y 1 sea 0.
#Now we need to define our constraints def l1_norm_constraint(x): return(((np.sum(np.abs(x))) - 1)) constraints = ({'type':'eq','fun':l1_norm_constraint})
Todos nuestros pesos deben estar entre -1 y 1. Esto se puede lograr definiendo límites para nuestro algoritmo.
#Now we need to define the bounds for our weights bounds = [(-1,1)] * 5
Realizando el procedimiento de optimización.
#Perform the optimization results = minimize(cost_function,weights,method="SLSQP",bounds=bounds,constraints=constraints)
Los resultados de nuestro procedimiento de optimización.
results
success: True
status: 0
fun: 0.0024308603411499208
x: [ 3.931e-01 1.138e-01 -5.991e-02 7.744e-02 -3.557e-01]
nit: 23
jac: [ 3.851e-04 2.506e-05 -3.083e-04 -6.868e-05 -3.186e-04]
nfev: 158
njev: 23
Almacenemos los valores de coeficientes óptimos que hemos calculado.
optimal_weights = results.x optimal_weights
También debemos almacenar los puntos óptimos del procedimiento.
optima_y = min(evaluation_history)
optima_x = evaluation_history.index(optima_y)
inputs = np.arange(0,len(evaluation_history))
Visualicemos el historial de rendimiento de nuestro algoritmo de optimización. Como podemos ver en el gráfico, nuestro algoritmo parece haber tenido dificultades al principio, durante las primeras 50 iteraciones. Sin embargo, parece haber sido capaz de encontrar un punto óptimo que maximiza el rendimiento de nuestra cartera.
plt.scatter(inputs,evaluation_history) plt.plot(optima_x,optima_y,'s',color='r') plt.axvline(x=optima_x,ls='--',color='red') plt.axhline(y=optima_y,ls='--',color='red') plt.title("Maximizing Returns")
Figura 5: Rendimiento de nuestro algoritmo de optimización SLSQP.
Verifiquemos que el valor absoluto de nuestros pesos sume 1, o en otras palabras, queremos validar que nuestra restricción de norma L1 no fue violada.
#Validate the weights add up to 1 np.sum(np.abs(optimal_weights))
Hay una forma intuitiva de interpretar los coeficientes óptimos. Si suponemos que queremos abrir 10 posiciones, primero multiplicaremos los coeficientes por 10. Luego realizaremos una división entera por 1 para eliminar cualquier decimal. Los enteros que nos sobran podrían interpretarse como el número de posiciones que debemos abrir en cada mercado. Nuestros datos parecen sugerir que abramos 3 posiciones largas en Broadcom, 1 posición larga en Cisco, 1 posición corta en Comcast, ninguna posición en Intel y 4 posiciones cortas en NVIDIA para maximizar nuestros rendimientos.
#Here's an intuitive way of understanding the data #If we can only open 10 positions, our best bet may be #3 buy positions in Broadcom #1 buy position in Cisco #1 sell position sell position in Comcast #No positions in Intel #4 sell postions in NVIDIA (optimal_weights * 10) // 1
Implementación en MQL5
Implementemos ahora nuestra estrategia comercial en MQL5. Comenzaremos definiendo primero las variables globales que usaremos en nuestra aplicación.
//+------------------------------------------------------------------+ //| NASDAQ IC AI.mq5 | //| Gamuchirai Zororo Ndawana | //| https://www.mql5.com/en/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Zororo Ndawana" #property link "https://www.mql5.com/en/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int rsi_handler,bb_handler; double bid,ask; int optimal_weights[5] = {3,1,-1,0,-4}; string stocks[5] = {"AVGO.NAS","CSCO.NAS","CMCSA.NAS","INTC.NAS","NVDA.NAS"}; vector current_close = vector::Zeros(1); vector rsi_buffer = vector::Zeros(1); vector bb_high_buffer = vector::Zeros(1); vector bb_mid_buffer = vector::Zeros(1); vector bb_low_buffer = vector::Zeros(1);
Importar la biblioteca comercial para ayudarnos a gestionar nuestras posiciones.
//+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade/Trade.mqh> CTrade Trade;
El usuario final de nuestro programa puede ajustar el comportamiento del Asesor Experto a través de las entradas que le permitimos controlar.
//+------------------------------------------------------------------+ //| User inputs | //+------------------------------------------------------------------+ input double profit_target = 1.0; //At this profit level, our position will be closed input int rsi_period = 20; //Adjust the RSI period input int bb_period = 20; //Adjust the Bollinger Bands period input double trade_size = 0.3; //How big should our trades be?
Cada vez que configuramos nuestro algoritmo comercial por primera vez, debemos asegurarnos de que los cinco símbolos de nuestros cálculos anteriores estén disponibles. De lo contrario, abortaremos el procedimiento de inicialización.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Validate that all the symbols we need are available if(!validate_symbol()) { return(INIT_FAILED); } //--- Everything went fine return(INIT_SUCCEEDED); }
Si nuestro programa ha sido eliminado del gráfico, deberíamos liberar los recursos que ya no utilizamos.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Release resources we no longer need release_resources(); }
Cada vez que recibimos precios actualizados, primero nos gustaría almacenar la oferta y la demanda actuales en nuestras variables definidas globalmente, verificar oportunidades comerciales y, finalmente, retirar cualquier ganancia que tengamos lista.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Update market data update_market_data(); //--- Check for a trade oppurtunity in each symbol check_trade_symbols(); //--- Check if we have an oppurtunity to take ourt profits check_profits(); }
La función encargada de retirar nuestras ganancias de la mesa iterará sobre todos los símbolos que tengamos en nuestra canasta. Si puede encontrar el símbolo con éxito, verificará si tenemos posiciones en ese mercado. Suponiendo que tenemos posiciones abiertas, verificaremos si la ganancia supera el objetivo de ganancia definido por el usuario, si lo hace, cerraremos nuestras posiciones. De lo contrario, seguiremos adelante.
//+------------------------------------------------------------------+ //| Check for opportunities to collect our profits | //+------------------------------------------------------------------+ void check_profits(void) { for(int i =0; i < 5; i++) { if(SymbolSelect(stocks[i],true)) { if(PositionSelect(stocks[i])) { if(PositionGetDouble(POSITION_PROFIT) > profit_target) { Trade.PositionClose(stocks[i]); } } } } }
Cada vez que recibimos precios actualizados, queremos almacenarlos en nuestras variables de alcance global porque estas variables pueden ser llamadas en varias partes de nuestro programa.
//+------------------------------------------------------------------+ //| Update markte data | //+------------------------------------------------------------------+ void update_market_data(void) { ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); }
Siempre que nuestro Asesor Experto no esté en uso, liberaremos los recursos que ya no necesite para garantizar una buena experiencia del usuario final.
//+-------------------------------------------------------------------+ //| Release the resources we no longer need | //+-------------------------------------------------------------------+ void release_resources(void) { ExpertRemove(); } //+------------------------------------------------------------------+
Al inicializar, verificamos si todos los símbolos que necesitábamos estaban disponibles. La función a continuación es responsable de esa tarea. Se repite sobre todos los símbolos que tenemos en nuestro conjunto de acciones. Si no logramos seleccionar ningún símbolo, la función devolverá falso y detendrá el procedimiento de inicialización. De lo contrario, la función devolverá verdadero.
//+------------------------------------------------------------------+ //| Validate that all the symbols we need are available | //+------------------------------------------------------------------+ bool validate_symbol(void) { for(int i=0; i < 5; i++) { //--- We failed to add one of the necessary symbols to the Market Watch window! if(!SymbolSelect(stocks[i],true)) { Comment("Failed to add ",stocks[i]," to the market watch. Ensure the symbol is available."); return(false); } } //--- Everything went fine return(true); }
Esta función es responsable de coordinar el proceso de apertura y gestión de posiciones en nuestra cartera. Iterará a través de todos los símbolos de nuestra matriz y verificará si tenemos posiciones abiertas en ese mercado y si deberíamos tener posiciones abiertas en ese mercado. Si deberíamos, pero no lo hacemos, la función iniciará el proceso de verificación de oportunidades para ganar exposición en ese mercado. De lo contrario, la función no hará nada.
//+------------------------------------------------------------------+ //| Check if we have any trade opportunities | //+------------------------------------------------------------------+ void check_trade_symbols(void) { //--- Loop through all the symbols we have for(int i=0;i < 5;i++) { //--- Select that symbol and check how many positons we have open if(SymbolSelect(stocks[i],true)) { //--- If we have no positions in that symbol, optimize the portfolio if((PositionsTotal() == 0) && (optimal_weights[i] != 0)) { optimize_portfolio(stocks[i],optimal_weights[i]); } } } }
La función de optimización de cartera toma dos parámetros: la acción en cuestión y los pesos atribuidos a esa acción. Si los pesos son positivos, la función llamará para iniciar un procedimiento para asumir una posición larga en ese mercado hasta que se cumpla el parámetro de peso; lo opuesto es cierto para los pesos negativos.
//+------------------------------------------------------------------+ //| Optimize our portfolio | //+------------------------------------------------------------------+ void optimize_portfolio(string symbol,int weight) { //--- If the weight is less than 0, check if we have any oppurtunities to sell that stock if(weight < 0) { if(SymbolSelect(symbol,true)) { //--- If we have oppurtunities to sell, act on it if(check_sell(symbol, weight)) { Trade.Sell(trade_size,symbol,bid,0,0,"NASDAQ IC AI"); } } } //--- Otherwise buy else { if(SymbolSelect(symbol,true)) { //--- If we have oppurtunities to buy, act on it if(check_buy(symbol,weight)) { Trade.Buy(trade_size,symbol,ask,0,0,"NASDAQ IC AI"); } } } }
Ahora debemos definir las condiciones bajo las cuales podemos entrar en una posición larga. Nos basaremos en una combinación de análisis técnico y acción del precio para cronometrar nuestras entradas. Solo entraremos en posiciones largas si los niveles de precios están por encima de la Banda de Bollinger superior, nuestros niveles de RSI están por encima de 70 y la acción del precio en marcos de tiempo más altos ha sido alcista. Asimismo, creemos que esto puede constituir una configuración de alta probabilidad, lo que nos permitiría alcanzar nuestros objetivos de ganancias, de forma segura. Por último, nuestra condición final es que el número total de posiciones que tenemos abiertas en ese mercado, no exceda nuestros niveles de asignación óptimos. Si se cumplen nuestras condiciones, entonces devolveremos verdadero, lo que le dará a la función "optimize_portfolio" autorización para ingresar en una posición larga.
//+------------------------------------------------------------------+ //| Check for oppurtunities to buy | //+------------------------------------------------------------------+ bool check_buy(string symbol, int weight) { //--- Ensure we have selected the right symbol SymbolSelect(symbol,true); //--- Load the indicators on the symbol bb_handler = iBands(symbol,PERIOD_CURRENT,bb_period,0,1,PRICE_CLOSE); rsi_handler = iRSI(symbol,PERIOD_CURRENT,rsi_period,PRICE_CLOSE); //--- Validate the indicators if((bb_handler == INVALID_HANDLE) || (rsi_handler == INVALID_HANDLE)) { //--- Something went wrong return(false); } //--- Load indicator readings into the buffers bb_high_buffer.CopyIndicatorBuffer(bb_handler,1,0,1); rsi_buffer.CopyIndicatorBuffer(rsi_handler,0,0,1); current_close.CopyRates(symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,0,1); //--- Validate that we have a valid buy oppurtunity if((bb_high_buffer[0] < current_close[0]) && (rsi_buffer[0] > 70)) { return(false); } //--- Do we allready have enough positions if(PositionsTotal() >= weight) { return(false); } //--- We can open a position return(true); }
Nuestra función "check_sell" funciona de manera similar a nuestra función check buy, excepto que primero multiplica el peso por menos 1 para que podamos contar fácilmente cuántas posiciones deberíamos tener abiertas en el mercado. La función procederá a verificar si el precio está por debajo de la banda baja de Bollinger y si la lectura del RSI es inferior a 30. Si se cumplen estas tres condiciones, también debemos asegurarnos de que la acción del precio en marcos temporales superiores nos permita ingresar en una posición corta.
//+------------------------------------------------------------------+ //| Check for oppurtunities to sell | //+------------------------------------------------------------------+ bool check_sell(string symbol, int weight) { //--- Ensure we have selected the right symbol SymbolSelect(symbol,true); //--- Negate the weight weight = weight * -1; //--- Load the indicators on the symbol bb_handler = iBands(symbol,PERIOD_CURRENT,bb_period,0,1,PRICE_CLOSE); rsi_handler = iRSI(symbol,PERIOD_CURRENT,rsi_period,PRICE_CLOSE); //--- Validate the indicators if((bb_handler == INVALID_HANDLE) || (rsi_handler == INVALID_HANDLE)) { //--- Something went wrong return(false); } //--- Load indicator readings into the buffers bb_low_buffer.CopyIndicatorBuffer(bb_handler,2,0,1); rsi_buffer.CopyIndicatorBuffer(rsi_handler,0,0,1); current_close.CopyRates(symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,0,1); //--- Validate that we have a valid sell oppurtunity if(!((bb_low_buffer[0] > current_close[0]) && (rsi_buffer[0] < 30))) { return(false); } //--- Do we have enough trades allready open? if(PositionsTotal() >= weight) { //--- We have a valid sell setup return(false); } //--- We can go ahead and open a position return(true); }
Fig 6: Prueba de nuestro algoritmo.
Conclusión
En nuestro debate, hemos demostrado cómo se puede determinar algorítmicamente el tamaño de su posición y la asignación de capital utilizando IA. Hay muchos aspectos diferentes de una cartera que podemos optimizar, como el riesgo (varianza) de una cartera, la correlación de nuestra cartera con el rendimiento de un índice de referencia de la industria (beta) y los rendimientos ajustados al riesgo de la cartera. En nuestro ejemplo, mantuvimos nuestro modelo simple y solo consideramos maximizar el rendimiento. Consideraremos muchas métricas importantes a medida que avancemos en esta serie. Sin embargo, este ejemplo simple nos permite comprender las ideas principales detrás de la optimización de cartera y cuando incluso progresamos para llegar a procedimientos de optimización complejos, el lector puede abordar el problema con confianza sabiendo que las ideas principales que hemos delineado aquí no cambiarán. Si bien no podemos garantizar que la información contenida en nuestro debate genere éxito en todo momento, sin duda vale la pena considerarla si usted realmente desea operar con múltiples símbolos de manera algorítmica.
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/15909





- 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