
Análise Múltipla de Símbolos com Python e MQL5 (Parte I): Fabricantes de Circuitos Integrados do NASDAQ
Existem muitas maneiras de um investidor diversificar seu portfólio. Além disso, há várias métricas diferentes que podem ser usadas como critério para avaliar o quão bem o portfólio foi otimizado. É improvável que qualquer investidor individual tenha tempo ou recursos suficientes para considerar cuidadosamente todas as opções antes de tomar uma decisão tão importante. Nesta série de artigos, vamos guiá-lo pelas inúmeras opções que estão à sua frente na jornada de negociar múltiplos símbolos simultaneamente. Nosso objetivo é ajudar você a decidir quais estratégias manter e quais podem não ser adequadas para você.
Visão Geral da Estratégia de Trading
Nesta discussão, selecionamos uma cesta de ações que estão fundamentalmente relacionadas entre si. Selecionamos 5 ações de empresas que projetam e vendem circuitos integrados em seu ciclo de negócios. Essas empresas são Broadcom, Cisco, Intel, NVIDIA e Comcast. Todas as 5 empresas estão listadas na National Association of Securities Dealers Automated Quotations (NASDAQ). A NASDAQ foi criada em 1971 e é a maior bolsa dos Estados Unidos em volume de negociações.
Os circuitos integrados se tornaram parte fundamental de nossa vida cotidiana. Esses chips eletrônicos permeiam todos os aspectos da vida moderna, desde os servidores proprietários da MetaQuotes que hospedam este site onde você está lendo este artigo, até o dispositivo que você está usando para ler — todos dependem de uma tecnologia que, muito provavelmente, foi desenvolvida por uma dessas 5 empresas. O primeiro circuito integrado do mundo foi desenvolvido pela Intel, batizado de Intel 4004, e lançado em 1971, o mesmo ano da fundação da bolsa NASDAQ. O Intel 4004 possuía aproximadamente 2.600 transistores, bem diferente dos chips modernos que facilmente têm bilhões de transistores..
Como somos motivados pela demanda global por circuitos integrados, queremos obter exposição inteligente ao mercado de chips. Dada uma cesta dessas 5 ações, vamos demonstrar como maximizar o retorno do seu portfólio alocando capital de forma criteriosa entre elas. Uma abordagem tradicional de distribuir igualmente o capital entre todas as 5 ações não é suficiente para os mercados modernos e voláteis. Em vez disso, vamos construir um modelo que nos indique se devemos comprar ou vender cada ação, e as quantidades ideais a serem negociadas. Ou seja, estamos usando os dados disponíveis para aprender, de forma algorítmica, o dimensionamento de posição e quantidades.
Visão Geral da Metodologia
Começamos buscando 100.000 linhas de dados de mercado M1 para cada uma das 5 ações da nossa cesta, a partir do nosso MetaTrader 5 Terminal usando a biblioteca Python do MetaTrader 5. Após converter os dados de preço para variações percentuais, realizamos uma análise exploratória dos dados de retornos de mercado.
Observamos níveis fracos de correlação entre as 5 ações. Além disso, nossos boxplots mostraram claramente que o retorno médio de cada ação estava próximo de 0. Também plotamos os retornos de cada ação de forma sobreposta, e foi possível observar claramente que os retornos da NVIDIA foram os mais voláteis. Por fim, criamos gráficos de dispersão pareados entre todas as 5 ações selecionadas, e infelizmente não foi possível identificar qualquer relação clara que pudesse ser aproveitada.
A partir daí, usamos a biblioteca SciPy para encontrar os pesos ideais para cada uma das 5 ações em nosso portfólio. Permitimos que todos os 5 pesos variem entre -1 e 1. Sempre que o peso do portfólio estiver abaixo de 0, o algoritmo está indicando venda e, inversamente, quando os pesos estiverem acima de 0, os dados sugerem compra.
Após calcular os pesos ideais do portfólio, integramos esses dados em nosso aplicativo de negociação para garantir que ele sempre mantenha um número ótimo de posições abertas em cada mercado. Nosso aplicativo de negociação é projetado para fechar automaticamente qualquer posição aberta se atingir um nível de lucro, especificado pelo usuário final.
Buscando os Dados
Para começar, vamos importar primeiro as bibliotecas necessárias.
#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
Agora, vamos inicializar o terminal MetaTrader 5.
#Initialize the terminal
mt5.initialize()
Defina a cesta de ações que desejamos 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"]
Vamos criar um data-frame para armazenar nossos dados 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))
Agora, vamos buscar nossos dados 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()
Vamos formatar nossos dados.
#Format the data set
returns.dropna(inplace=True)
returns.reset_index(inplace=True,drop=True)
returns
Por fim, multiplique os dados por 100 para salvá-los como percentuais.
#Convert the returns to percentages returns = returns * 100 returns
Análise Exploratória dos Dados
Às vezes, podemos visualizar a relação entre as variáveis do sistema. Vamos analisar os níveis de correlação em nossos dados para ver se há alguma combinação linear da qual possamos tirar proveito. Infelizmente, nossos níveis de correlação não são impressionantes e, até agora, não parece haver dependências lineares para explorarmos.
#Vamos analisar se há alguma correlação nos dados sns.heatmap(returns.corr(),annot=True)
Fig 1: Nosso mapa de calor de correlação
Vamos analisar gráficos de dispersão pareados de nossos dados. Ao lidar com grandes conjuntos de dados, relações não triviais podem facilmente passar despercebidas. Os gráficos de pares minimizam a chance disso acontecer. Infelizmente, não houve relações facilmente observáveis nos dados reveladas por nossos gráficos.
#Vamos criar gráficos de pares dos nossos dados sns.pairplot(returns)
Fig 2: Alguns dos nossos gráficos de dispersão pareados
Ao plotar os retornos observados nos dados, vemos que a NVIDIA aparenta ter os retornos mais voláteis.
#Lets also visualize our returns
returns.plot()
Fig 3: Plotando nossos retornos de mercado
Visualizando nossos retornos de mercado em boxplots, fica claro que o retorno médio de mercado é 0.
#Vamos tentar criar boxplots sns.boxplot(returns)
Fig 4: Visualizando nossos retornos de mercado em boxplots
Otimização de Portfólio
Agora estamos prontos para começar a calcular os pesos ótimos de alocação de capital para cada ação. Inicialmente, atribuiremos nossos pesos de forma aleatória. Além disso, também criaremos uma estrutura de dados para armazenar o progresso do nosso algoritmo de otimização.
#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 = []
A função objetivo do nosso procedimento de otimização será o retorno do nosso portfólio com os pesos definidos. Observe que os retornos do nosso portfólio serão calculados usando a média geométrica dos retornos dos ativos. Optamos por empregar a média geométrica em vez da média aritmética porque, ao lidar com valores positivos e negativos, calcular a média deixa de ser uma tarefa trivial. Se abordássemos esse problema de maneira casual e utilizássemos a média aritmética, poderíamos facilmente calcular um retorno de portfólio igual a 0. Podemos usar algoritmos de minimização para problemas de maximização multiplicando o retorno do portfólio por menos 1 antes de retorná-lo para o algoritmo de otimização.
#Agora vamos nos preparar para maximizar nossos retornos #Primeiro, precisamos definir a função de custo def cost_function(x): #Primeiro, precisamos calcular os retornos do portfólio com os pesos sugeridos portfolio_returns = np.dot(returns,x) geom_mean = ((np.prod( 1 + portfolio_returns ) ** (1.0/99999.0)) - 1) #Vamos acompanhar o desempenho do nosso algoritmo evaluation_history.append(-geom_mean) return(-geom_mean)
Agora, vamos definir a restrição que garante que a soma dos pesos seja igual a 1. Observe que apenas alguns procedimentos de otimização do SciPy suportam restrições de igualdade. Restrições de igualdade informam ao módulo SciPy que queremos que essa função seja igual a 0. Portanto, queremos que a diferença entre o valor absoluto dos pesos e 1 seja igual a 0.
#Agora precisamos definir nossas restrições def l1_norm_constraint(x): return(((np.sum(np.abs(x))) - 1)) constraints = ({'type':'eq','fun':l1_norm_constraint})
Todos os nossos pesos devem estar entre -1 e 1. Isso pode ser imposto definindo limites (bounds) para nosso algoritmo.
#Now we need to define the bounds for our weights bounds = [(-1,1)] * 5
Executando o procedimento de otimização.
#Perform the optimization results = minimize(cost_function,weights,method="SLSQP",bounds=bounds,constraints=constraints)
Os resultados do nosso procedimento de otimização.
Resultados
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
Vamos armazenar os valores ótimos de coeficientes que calculamos.
optimal_weights = results.x optimal_weights
Também devemos armazenar os pontos ótimos do procedimento.
optima_y = min(evaluation_history)
optima_x = evaluation_history.index(optima_y)
inputs = np.arange(0,len(evaluation_history))
Vamos visualizar o histórico de desempenho do nosso algoritmo de otimização. Como podemos ver pelo gráfico, nosso algoritmo parece ter tido dificuldades no início, nas primeiras 50 iterações. No entanto, parece ter conseguido encontrar um ponto ótimo que maximiza os retornos do nosso portfólio.
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")
Fig 5: Desempenho do nosso algoritmo de otimização SLSQP
Vamos verificar se o valor absoluto da soma dos pesos é igual a 1, ou seja, queremos validar que nossa restrição de norma L1 não foi violada.
#Validate the weights add up to 1 np.sum(np.abs(optimal_weights))
Há uma forma intuitiva de interpretar os coeficientes ótimos. Se assumirmos que queremos abrir 10 posições, primeiro multiplicaremos os coeficientes por 10. Em seguida, vamos realizar uma divisão inteira por 1 para eliminar as casas decimais. Os inteiros restantes podem ser interpretados como o número de posições que devemos abrir em cada mercado. Nossos dados parecem sugerir que devemos abrir 3 posições compradas em Broadcom, 1 posição comprada em Cisco, 1 posição vendida em Comcast, nenhuma posição em Intel e 4 posições vendidas em NVIDIA para maximizar nossos retornos.
#Aqui está uma forma intuitiva de entender os dados #Se só podemos abrir 10 posições, nossa melhor escolha pode ser #3 posições de compra em Broadcom #1 posição de compra em Cisco #1 posição de venda em Comcast #Nenhuma posição em Intel #4 posições de venda em NVIDIA (optimal_weights * 10) // 1
Implementação em MQL5
Agora vamos implementar nossa estratégia de negociação em MQL5. Vamos começar definindo as variáveis globais que usaremos em nossa aplicação.
//+------------------------------------------------------------------+ //| 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);
Importando a biblioteca de negociação para nos ajudar a gerenciar nossas posições.
//+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade/Trade.mqh> CTrade Trade;
O usuário final do nosso programa pode ajustar o comportamento do Expert Advisor através dos parâmetros de entrada que 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?
Sempre que nosso algoritmo de negociação for configurado pela primeira vez, precisamos garantir que todos os 5 símbolos dos nossos cálculos anteriores estejam disponíveis. Caso contrário, abortaremos o procedimento de inicialização.
//+------------------------------------------------------------------+ //| 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); }
Se nosso programa for removido do gráfico, devemos liberar os recursos que não estamos mais utilizando.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Release resources we no longer need release_resources(); }
Sempre que recebermos preços atualizados, primeiro queremos armazenar o bid e o ask atuais em nossas variáveis globais definidas, verificar oportunidades de negociação e, por fim, realizar qualquer lucro já disponível.
//+------------------------------------------------------------------+ //| 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(); }
A função responsável por realizar nossos lucros irá iterar sobre todos os símbolos da nossa cesta. Se conseguir encontrar o símbolo com sucesso, irá verificar se temos posições abertas nesse mercado. Supondo que tenhamos posições abertas, verificaremos se o lucro ultrapassa o alvo de lucro definido pelo usuário; se ultrapassar, fecharemos nossas posições. Caso contrário, iremos para o próximo.
//+------------------------------------------------------------------+ //| 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]); } } } } }
Sempre que recebermos preços atualizados, queremos armazená-los em nossas variáveis globais, pois essas variáveis podem ser chamadas em várias partes do programa.
//+------------------------------------------------------------------+ //| Update markte data | //+------------------------------------------------------------------+ void update_market_data(void) { ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); }
Sempre que nosso Expert Advisor não estiver em uso, liberaremos os recursos que não são mais necessários, garantindo uma boa experiência ao usuário final.
//+-------------------------------------------------------------------+ //| Release the resources we no longer need | //+-------------------------------------------------------------------+ void release_resources(void) { ExpertRemove(); } //+------------------------------------------------------------------+
Na inicialização, verificamos se todos os símbolos necessários estão disponíveis. A função abaixo é responsável por essa tarefa. Ela itera por todos os símbolos do nosso array de ações. Se não conseguirmos selecionar algum símbolo, a função retorna falso e interrompe o procedimento de inicialização. Caso contrário, a função retorna verdadeiro.
//+------------------------------------------------------------------+ //| 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 função é responsável por coordenar o processo de abertura e gerenciamento de posições no nosso portfólio. Ela vai iterar por todos os símbolos do nosso array e verificar se temos posições abertas naquele mercado e se deveríamos ter posições abertas naquele mercado. Se deveríamos, mas não temos, a função iniciará o processo de verificação de oportunidades para ganhar exposição naquele mercado. Caso contrário, não fará 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]); } } } }
A função de otimização do portfólio recebe 2 parâmetros: a ação em questão e os pesos atribuídos a essa ação. Se os pesos forem positivos, a função iniciará o procedimento para assumir uma posição comprada nesse mercado até que o parâmetro de peso seja atingido; o contrário é verdadeiro para 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"); } } } }
Agora precisamos definir as condições sob as quais podemos entrar em uma posição comprada (long). Vamos nos basear em uma combinação de análise técnica e ação do preço para definir nossas entradas. Só entraremos em posições compradas se os preços estiverem acima da banda superior das Bollinger Bands, nosso RSI estiver acima de 70 e a ação do preço em time-frames maiores for altista. Da mesma forma, acreditamos que isso pode constituir uma configuração de alta probabilidade, permitindo que alcancemos nossas metas de lucro com segurança. Por fim, nossa última condição é que o número total de posições abertas naquele mercado não ultrapasse nossos níveis de alocação ótima. Se as condições forem satisfeitas, retornaremos verdadeiro, o que dará à função "optimize_portfolio" autorização para entrar em uma posição comprada.
//+------------------------------------------------------------------+ //| 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); }
Nossa função "check_sell" funciona de forma semelhante à função de compra, exceto pelo fato de multiplicar o peso por menos 1 primeiro, para que possamos contar facilmente quantas posições devemos ter abertas no mercado. A função irá verificar se o preço está abaixo da Banda Inferior das Bollinger Bands e se o RSI está abaixo de 30. Se essas três condições forem atendidas, também precisamos garantir que a ação do preço em time-frames maiores permita que entremos em uma posição vendida (short).
//+------------------------------------------------------------------+ //| 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: Testando nosso algoritmo em forward
Conclusão
Em nossa discussão, demonstramos como você pode determinar, de forma algorítmica, o dimensionamento de posição e a alocação de capital usando IA. Há muitos aspectos diferentes de um portfólio que podemos otimizar, como o risco (variância) do portfólio, a correlação do nosso portfólio com o desempenho de um benchmark da indústria (beta) e os retornos ajustados ao risco do portfólio. No nosso exemplo, mantivemos o modelo simples e consideramos apenas a maximização do retorno. Vamos considerar muitas métricas importantes à medida que avançarmos nesta série. No entanto, esse exemplo simples permite compreender as ideias principais por trás da otimização de portfólio e, mesmo que avancemos para procedimentos mais complexos, o leitor pode abordar o problema com confiança sabendo que os princípios fundamentais apresentados aqui não mudam. Embora não possamos garantir que as informações contidas em nossa discussão gerarão sucesso sempre, certamente vale a pena considerar se você realmente leva a sério operar múltiplos símbolos de forma algorítmica.
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/15909





- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso