English Русский 中文 Español Deutsch 日本語
preview
Reimaginando Estratégias Clássicas (Parte IV): SP500 e Notas do Tesouro dos EUA

Reimaginando Estratégias Clássicas (Parte IV): SP500 e Notas do Tesouro dos EUA

MetaTrader 5Exemplos |
186 1
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Introdução

No nosso artigo anterior, discutimos uma possível estratégia de negociação do S&P 500 que se basearia em usar uma seleção de ações que mantinham altos pesos dentro do índice. No artigo de hoje, vamos analisar uma abordagem alternativa de negociação do S&P 500 usando o rendimento das Notas do Tesouro. Há muitos anos, sempre que os investidores se sentiam avessos ao risco, normalmente retiravam seu dinheiro de investimentos arriscados, como ações, e preferiam guardar seu dinheiro em investimentos mais seguros, como títulos e notas do tesouro. Por outro lado, quando os investidores ganhavam confiança nos mercados, tendiam a retirar seu dinheiro dos investimentos seguros, como títulos, e preferiam investir seu dinheiro no mercado de ações.

Analistas fundamentais perceberam ao longo dos anos que essa correlação entre os movimentos do S&P 500 e o movimento nos rendimentos do Tesouro parece ser oposta. Parece ser uma correlação negativa, como se dissesse que, à medida que os investidores investem mais em ações, tendem a investir menos em títulos e notas do tesouro.


Visão Geral da Estratégia de Negociação

O S&P 500 é um benchmark significativo do desempenho da economia industrial da América em um nível muito amplo. Por outro lado, as notas do Tesouro são consideradas os investimentos mais seguros da Terra. Quando um investidor compra um título ou uma nota do Tesouro, ele está essencialmente emprestando dinheiro para o governo que emitiu essa nota do Tesouro. Cada nota do Tesouro paga cupons de juros que são mostrados na face do título.

Quando a demanda por títulos é baixa, o rendimento do título sobe. Isso é feito para reacender a demanda. Então, à medida que menos investidores compram títulos, vemos o rendimento subir. De maneira geral, os analistas fundamentais têm usado essa relação a seu favor há muito tempo. Se estivessem negociando no S&P 500, procurariam sinais de enfraquecimento da tendência.

Por exemplo, se os rendimentos dos títulos começassem a subir, os analistas fundamentais saberiam que os investidores não estão comprando títulos; em vez disso, podem estar colocando seu dinheiro em ativos que proporcionam uma taxa de retorno mais alta, como as ações.

No entanto, se um analista fundamental notasse que o rendimento dos títulos estava caindo, isso seria um sinal de que há uma demanda muito alta por títulos. Isso indicaria que o analista fundamental provavelmente não deveria investir no mercado de ações ainda, pois o sentimento geral do mercado é avesso ao risco e as estratégias fundamentais usariam isso para entrar e sair de suas posições.

No artigo de hoje, queremos ver se essa relação é estatisticamente significativa e se é confiável para construirmos uma estratégia de negociação com base nessa relação. Vamos começar.


Visão Geral da Metodologia

Para examinar empiricamente os méritos dessa estratégia, vamos ajustar vários modelos para prever o preço de fechamento do SP500 usando dados comuns de OHLC do próprio índice. A partir daí, vamos observar a mudança na precisão quando tentarmos treinar os modelos para prever o mesmo alvo, mas desta vez, os modelos terão acesso apenas aos dados de OHLC da Nota do Tesouro dos EUA de 5 anos. Nossas observações nos levaram a acreditar que os investidores podem estar melhor utilizando dados do índice SP500. Os níveis de desempenho do nosso modelo caíram em todas as áreas, e, além disso, a variância em nossos níveis de erro aumentou quando tentamos usar dados do Tesouro. Empregamos validação cruzada de séries temporais sem embaralhamento aleatório para comparar modelos de diferentes complexidades.

Após observar as mudanças nos níveis de erro, identificamos o Regressor SGD como o modelo de melhor desempenho, e então realizamos a seleção de características no modelo. Nenhum dos dados relacionados às Notas do Tesouro foi selecionado pelo nosso seletor de características, o que indica que a relação pode não ser estatisticamente significativa. Embora nesse ponto tivéssemos evidências suficientes de que poderíamos descartar os dados das Notas do Tesouro, mantivemos os dados e continuamos a construir nosso modelo.

Na nossa etapa final antes de exportar o modelo para o formato ONNX, tentamos ajustar os hiperparâmetros do modelo. Usamos o algoritmo L-BFGS-B (Limited-Memory Broyden-Fletcher-Goldfarb-Shanno) na tentativa de encontrar configurações ideais de parâmetros para nosso modelo. Nosso objetivo era superar o desempenho das configurações padrão do modelo. Infelizmente, acabamos ajustando demais nosso modelo aos dados de treinamento e, portanto, não conseguimos superar o modelo padrão.


Análise Exploratória de Dados em Python

Para buscar dados do nosso Terminal MetaTrader 5, criei um script para gravar os dados históricos do mercado em formato CSV para nós. Anexo o script junto. Basta arrastá-lo e soltá-lo no gráfico, e ele irá gravar os dados para nós.

Uma vez que os dados estejam preparados, começamos importando as bibliotecas que precisamos.

#Import the libraries we need 
import pandas as pd
import numpy as np
import seaborn as sns

Uma vez feito isso, vamos ler nossos dados.

#Read in the data
SP500 = pd.read_csv("/home/volatily/market_data/Market Data US SP 500.csv")
T5Y = pd.read_csv("/home/volatily/market_data/Market Data UST05Y_U4.csv")

Precisamos definir até onde no futuro gostaríamos de fazer a previsão. Então, neste exemplo, vamos prever 20 passos para o futuro.

#How far into the future should we forecast?
look_ahead = 20

Agora, também precisamos garantir que os dados comecem com o dia mais antigo primeiro e o dia mais recente nos dados inteiros deve ser descartado.

#Make sure the data starts with the oldest day first
SP500 = SP500[::-1].reset_index().set_index("Time").drop(columns=["index"])
T5Y = T5Y[::-1].reset_index().set_index("Time").drop(columns=["index"])

Uma vez feito isso, vamos rotular os dados. Teremos um rótulo, que seria o preço de fechamento futuro do S&P 500, 20 passos à frente. E então, o segundo alvo binário está sendo criado apenas para fins de plotagem.

#Insert the label
SP500["Target SP500"] = SP500["Close"].shift(-look_ahead)
SP500["Binary Target SP500"] = 0
SP500.loc[SP500["Close"] < SP500["Target SP500"],"Binary Target SP500"] = 1
SP500.dropna(inplace=True)

Agora que fizemos isso, vamos combinar os dois dados. Vamos combinar os dados do S&P 500 e o rendimento do Tesouro de cinco anos em um único DataFrame combinado.

#Merge the data
merged_df = pd.merge(SP500,T5Y,how="inner",left_index=True,right_index=True,suffixes=(" SP500"," T5Y"))

E podemos observar o quadro de dados mesclado.

#Vamos observar o quadro de dados mesclado
merged_df

Nosso quadro de dados mesclado

Figura 1: Nosso quadro de dados mesclado

Também podemos analisar a correlação no quadro de dados mesclado. Podemos observar que os níveis de correlação estão em torno de 0,1, o que não é forte.

#Merged data frame correlation
merged_df.corr()


Níveis de correlação.

Figura 2: Níveis de correlação no nosso quadro de dados mesclado

No entanto, altos níveis de correlação não implicam necessariamente que exista uma relação definida entre as duas variáveis que estamos analisando. Tampouco implica que uma variável está causando a outra variável. Altos níveis de correlação podem indicar que há uma causa comum que está afetando esses dois mercados.

Eu fiz um gráfico de dispersão com o tempo no eixo x, e no eixo y está o preço de abertura do S&P 500. E então, usei os alvos binários para colorir os pontos ao longo do gráfico de dispersão. Observe que os pontos azuis e laranja naturalmente se agrupam, o que pode nos indicar que o tempo separa bem os dados. Lembre-se de que nosso alvo binário nos diz o que vai acontecer 20 passos à frente, os pontos azuis significam que o preço caiu nos próximos 20 passos e os pontos laranja indicam que o oposto aconteceu.

#It appears that one variable that separates the data well is time
sns.scatterplot(data=merged_df,x="Candle",y="Open SP500",hue="Binary Target SP500")

O tempo separa bem nossos dados

Figura 3: Nossos dados parecem bem separados no tempo

Parece que o tempo separa muito bem os dados. No entanto, quando tentamos usar outras variáveis para separar os dados, como, por exemplo, aqui, criamos um gráfico de dispersão com o preço de abertura do S&P 500 contra a abertura do rendimento do tesouro de cinco anos. Vemos que obtemos este gráfico de dispersão mal separado, onde há muitos pontos sobrepostos, e não há separação clara alguma.

#It appears that one variable that separates the data well is time
sns.scatterplot(data=merged_df,x="Open T5Y",y="Open SP500",hue="Binary Target SP500")

Separação ruim

Figura 4: Níveis de separação ruim


Seleção de modelo

Agora que fizemos isso, vamos passar para a modelagem da relação entre o S&P 500 e os rendimentos dos títulos do Tesouro. Vamos importar os módulos que precisamos do scikit-learn.

#Import the libraries we need
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import Lasso
from sklearn.linear_model import SGDRegressor
from sklearn.svm import LinearSVR
from sklearn.ensemble import RandomForestRegressor
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.ensemble import BaggingRegressor
from sklearn.ensemble import AdaBoostRegressor
from sklearn.neural_network import MLPRegressor
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import root_mean_squared_error
from sklearn.preprocessing import RobustScaler
import time
from numpy.random import rand,randn
from scipy.optimize import minimize

E então, vamos nos preparar para criar um objeto de divisão de série temporal. Primeiro, definimos o número de divisões que queremos, e depois criamos o próprio objeto de divisão de série temporal.

#Define the number of splits we want
splits = 10
#Create the time series split object
tscv = TimeSeriesSplit(n_splits = splits, gap=look_ahead)

E como temos vários modelos, vamos armazená-los em uma lista.

#Store the models in a list
models = [LinearRegression(),
         Lasso(),
         SGDRegressor(),
         LinearSVR(),
         RandomForestRegressor(),
         GradientBoostingRegressor(),
         BaggingRegressor(),
         AdaBoostRegressor(),
         MLPRegressor(hidden_layer_sizes=(10,4),early_stopping=True),
         ]

Vou definir uma função para inicializar nossos modelos, e a função se chama "initialize_models".

#Define a function to initialize our models
def initialize_models():
    models = [LinearRegression(),
         Lasso(),
         SGDRegressor(),
         LinearSVR(),
         RandomForestRegressor(),
         GradientBoostingRegressor(),
         BaggingRegressor(),
         AdaBoostRegressor(),
         MLPRegressor(hidden_layer_sizes=(10,4),early_stopping=True),
         ]

E então, também precisamos de data frames para armazenar nossos níveis de erro. Portanto, precisamos de três data frames. O primeiro data frame armazenará nossos níveis de erro quando estivermos apenas usando dados ordinários de abertura, máxima, mínima e fechamento do S&P 500, o segundo data frame armazenará nossos níveis de erro quando estivermos tentando prever o S&P 500 apenas com base nos rendimentos dos títulos do Tesouro. E o último data frame armazenará nossos níveis de erro ao usar todos os dados que temos.

#Create 3 dataframes to measure our performance
#Before we do that, we will define the columns and idexes
columns = ["Linear Regression",
          "Lasso",
          "SGD Regressor",
          "Linear SVR",
          "Random Forest Regressor",
          "Gradient Boosting Regressor",
          "Bagging Regressor",
          "Ada Boost Regressor",
          "MLP Regressor"]
indexes = np.arange(0,10)


#First dataframe stores our error levels using just the ordinary SP500 OHCL
SP500_error = pd.DataFrame(columns=columns,index=indexes)
#Second dataframe stores our error levels using just the ordinary Treasury Yield OHCL
TY5_error = pd.DataFrame(columns=columns,index=indexes)
#Last dataframe stores our error levels using all the data we have
total_error = pd.DataFrame(columns=columns,index=indexes)

Agora vamos definir nossos inputs e nosso alvo.

#Now we will define the inputs and target
target = "Target SP500"
predictors = ["Open T5Y",
              "Close T5Y",
              "High T5Y",
              "Low T5Y",
              "Open SP500",
              "Close SP500",
              "High SP500",
              "Low SP500"
             ]

E então, vamos redefinir o índice do nosso quadro de dados mesclado.

#Reset the index
merged_df.reset_index(inplace=True)

E então, vamos redefinir o índice do nosso quadro de dados mesclado. Então, simplesmente instanciamos o robust scaler, chamamos a função de transformação e passamos o quadro de dados mesclado para a função de ajuste e transformação. Tudo isso está envolvido dentro de um novo objeto de quadro de dados que vamos criar usando o pandas.

#Scale the data
scaled_data = pd.DataFrame(RobustScaler().fit_transform(merged_df.loc[:,predictors]),columns=predictors,index=np.arange(0,merged_df.shape[0]))

Tudo isso está envolvido dentro de um novo objeto de quadro de dados que vamos criar usando o pandas. A maneira mais fácil de fazer isso foi usando um loop aninhado. Portanto, o primeiro loop for itera sobre todos os modelos que temos, e o segundo loop irá validar cruzadamente cada modelo individualmente. Portanto, ajustaremos o modelo de regressão linear, depois ajustaremos o lasso e assim por diante.

#Now we will perform cross validation
#First we iterate over all the models we have
for j in np.arange(0,len(models)):
    for i,(train,test) in enumerate(tscv.split(merged_df)):
        #Prepare the models
        initialize_models()
        #Prepare the data
        X_train = scaled_data.loc[train[0]:train[-1],predictors]
        X_test = scaled_data.loc[test[0]:test[-1],predictors]
        y_train = merged_df.loc[train[0]:train[-1],target]
        y_test = merged_df.loc[test[0]:test[-1],target]
        #Now fit each model and measure its accuracy
        models[j].fit(X_train,y_train)
        SP500_error.iloc[i,j] = root_mean_squared_error(y_test,models[j].predict(X_test))
        print(f"Completed fitting model {models[j]}")
Modelo LinearRegression() ajustado com sucesso
Modelo LinearRegression() ajustado com sucesso
Modelo LinearRegression() ajustado com sucesso
Modelo LinearRegression() ajustado com sucesso
Modelo LinearRegression() ajustado com sucesso

A partir daí, podemos ver os níveis de erro do S&P 500, e parece que a regressão linear foi um dos melhores modelos de desempenho neste caso, seguida pelo SGD Regressor. A rede neural teve um desempenho bastante ruim. Na verdade, ela provavelmente se beneficiaria muito de um ajuste de parâmetros.

SP500_error

Níveis de erro do SP500

Figura 5: Níveis de erro ao usar dados ordinários do SP500 (abertura, máxima, mínima e fechamento)

Agora passamos para o rendimento do tesouro de cinco anos. Neste caso específico, todos os nossos modelos tiveram um desempenho ruim. No entanto, o Random Forest Regressor parece ter um desempenho bastante bom.

TY5_error

Níveis de erro do rendimento do tesouro

Figura 6: Níveis de erro ao confiar nos rendimentos dos títulos do tesouro

E então, por último, temos o erro total ao usar todos os dados disponíveis, parece que o regressor de descida do gradiente estocástico (SGD) tem um desempenho razoavelmente bom e, por esses motivos, selecionei o regressor SGD como o modelo de melhor desempenho.

total_error

Níveis de erro total

Figura 7: Níveis de erro quando usamos todos os dados disponíveis


Seleção de Características

Agora vamos realizar a seleção de características para ver se o nosso computador também acha que os dados de rendimento do tesouro são importantes. Se o seletor de características remover os dados relacionados ao rendimento do tesouro, isso pode ser motivo de preocupação para a nossa estratégia, pois pareceria que a relação não é confiável. No entanto, se o seletor de características mantiver os dados de rendimento do tesouro, isso pode ser um bom sinal.

#Feature selection
from mlxtend.feature_selection import SequentialFeatureSelector as SFS

#Get the best model
model = SGDRegressor()

Criamos o objeto de seletor de características sequenciais e passamos o modelo que gostaríamos de usar. A partir daí, instruímos o algoritmo para que ele possa selecionar tantas características quanto necessário. Poderíamos ter especificado que ele deveria selecionar cinco características, mas eu queria que ele selecionasse o número que calculasse ser importante. Definimos forward como true, o que significa que ele realizará a seleção para frente e, a partir daí, passamos CV igual a cinco, o que significa que utilizaremos validação cruzada com cinco divisões. A partir daí, passamos n-jobs igual a -1, isso permite que o seletor de características execute essa tarefa em paralelo.

#Let us perform feature selection for the best model we have
sfs_sgd_regressor = SFS(model,
                            (1,8),
                            forward=True,
                            cv=5,
                            n_jobs=-1,
                            scoring="neg_mean_squared_error"
                           )

A partir daí, ajustamos o seletor de características.

#Fit the feature selector
sfs_1 = sfs_sgd_regressor.fit(scaled_data.loc[:,predictors],merged_df.loc[:,target])

Quando agora olhamos para quais características foram as mais importantes para o nosso modelo, vemos que, infelizmente, nenhuma das características relacionadas ao rendimento do tesouro. Os rendimentos foram selecionados, apenas selecionaram o fechamento, a máxima e a mínima do S&P 500. Portanto, isso pode indicar que a relação não é tão estável, e é bem conhecido que a correlação entre os rendimentos dos títulos do tesouro e o S&P 500 pode ser quebrada de tempos em tempos.

#Which features were most important to our model?
sfs_1.k_feature_names_
('Close SP500', 'High SP500', 'Low SP500')

Ainda vamos tentar otimizar nosso modelo e ver o quanto de desempenho conseguimos obter.

#None the less, let us attempt to optimize the model
from scipy import optimize

E a partir daí, vamos criar dois conjuntos de dados dedicados. Um para treinar e otimizar o modelo, e o outro para validação. No conjunto de validação, vamos comparar o desempenho do nosso modelo otimizado com o desempenho de um modelo padrão, que está usando configurações padrão. Queremos tentar superar os níveis de erro padrão.

#Create a training and validation set
scaled_data = merged_df.loc[:,predictors]
scaled_data = (scaled_data - scaled_data.mean()) / (scaled_data.std())
#Create the two datasets
train_data , test_data = scaled_data.loc[:(scaled_data.shape[0]//2),:],scaled_data.loc[(scaled_data.shape[0]//2):,:]

Observe que, desta vez, estou usando uma técnica de escalonamento diferente, da primeira vez usei apenas o robust scaler. Desta vez, empregamos uma técnica de escalonamento muito comum, onde subtraímos a média de cada coluna e depois dividimos cada coluna pelo seu desvio padrão.

#Let's write out the column mean and standard deviations
#We'll store the mean first 
#Then the standard deviation
scale_factors = pd.DataFrame(columns=predictors,index=(0,1))
#Save the mean and std value of each respective column
for i in (np.arange(0,len(predictors))):
    #Calculate and store the values of each column mean and std
    scale_factors.iloc[0,i] = merged_df.loc[:,predictors[i]].mean()
    scale_factors.iloc[1,i] = merged_df.loc[:,predictors[i]].std()

#Inspect the data
scale_factors

Fatores de escala

Figura 8: Nossa média e desvio padrão para cada coluna

Os valores médios e os desvios padrão que calculamos para cada coluna são significativos, e precisaremos desses dados quando estivermos trabalhando novamente no MQL5, então estou salvando os dados no formato CSV.

#Write it out to csv format
scale_factors.to_csv("/home/volatily/.wine/drive_c/Program Files/MetaTrader 5/MQL5/Files/sp500_treasury_yields_scale.csv")


Ajustando o Modelo de Regressão SGD

Agora, vamos tentar ajustar o modelo, começamos definindo a função objetivo. A função objetivo, neste caso, será o nível de RMSE de treinamento, e queremos minimizar nossos níveis de RMSE nos dados de treinamento. No entanto, esse procedimento é uma espada de dois gumes. Quaisquer hiperparâmetros que minimizem nosso erro no conjunto de treinamento não são garantidos para minimizar nosso erro no conjunto de validação!

#Define the objective function 
def objective(x):
    #Initialize the model with the new parameters
    model = SGDRegressor(alpha=x[0],shuffle=False,eta0=x[1])
    #We need a dataframe to store our current model accuracy levels
    current_accuracy = pd.DataFrame(index=np.arange(0,splits),columns=["Error"])
    #Now we perform cross validation
    for i,(train,test) in enumerate(tscv.split(train_data)):
        #Split the data into a training set and test set
        X_train = train_data.loc[train[0]:train[-1],predictors]
        X_test  = train_data.loc[test[0]:test[-1],predictors]
        y_train = merged_df.loc[train[0]:train[-1],target]
        y_test  = merged_df.loc[test[0]:test[-1],target]
        #Fit the model
        model.fit(X_train,y_train)
        #Record the accuracy
        current_accuracy.iloc[i,0] = root_mean_squared_error(y_test,model.predict(X_test))
    #Return the model accuracrcy
    return(current_accuracy.iloc[:,0].mean())

Então, como sempre, vamos começar fazendo uma busca linear para termos uma ideia de onde os valores ótimos podem estar. Começamos fazendo uma busca linear normal, e levou 41 segundos para completar a busca.

#Let's optimize our model
#Let us measure how much time this takes.
start = time.time()

#Create a dataframe to measure the error rates
starting_point_error = pd.DataFrame(index=np.arange(0,21),columns=["Average CV RMSE"])
starting_point_error["Iteration"] = np.arange(0,21)

#Let us first find a good starting point for our optimization algorithm
for i in np.arange(0,21):
    #Set a new starting point
    new_starting_point = (10.0 ** -i)
    #Store error rates
    starting_point_error.iloc[i,0] = objective([new_starting_point  ,new_starting_point]) 

#Record the time stamp at the end
stop = time.time()

#Report the amount of time taken
print(f"Completed in {stop - start} seconds")
Concluído em 41.863527059555054 segundos

Pelos resultados da nossa busca linear, parece que cruzamos os pontos ótimos logo na primeira iteração.

starting_point_error["alpha"] = 0
starting_point_error["eta0"] = 0

for i in np.arange(0,21):
    starting_point_error.loc[i,"alpha"] = (10.0 ** -i)
    starting_point_error.loc[i,"eta0"] = (10.0 ** -i)

starting_point_error

Resultados da nossa busca linear

Fig 9: Resultados da nossa busca linear

Podemos também plotar essas informações visualmente, como você pode ver, elas formam quase um taco de hóquei invertido, com o erro mais baixo no início e depois nosso erro apenas continua aumentando.

#Vamos visualizar nossos níveis de erro sns.lineplot(data=starting_point_error,x="Iteration",y="Average CV RMSE").set(title="Otimizar nosso Regressor SGD nos Dados de Treinamento")

Fig 10: Plotando nossos níveis de erro

Fig 10: Visualizando nossos níveis de erro

Agora que temos uma ideia do que parece ser ótimo, podemos realizar uma busca local na região que parece ser ótima. Vamos usar o algoritmo L-BGFS-B para encontrar esses pontos ótimos. Primeiro, vamos selecionar pontos aleatórios da região que parece ótima.

#Now let us perform a local search in the space that appears optimal
pt = abs(((10 ** -2) + rand(2) * ((1) - (10 ** -2))))
pt

array([0.94169659, 0.33068772])

Agora, vamos tentar otimizar nosso modelo para os dados de treinamento.

#Let's try optimize our model
start = time.time()
bounds = ((0.01,1),(0.01,1))
result = minimize(objective,pt,bounds=bounds,method="L-BFGS-B")
stop = time.time()
print(f"Task completed in {stop - start} seconds")
Tarefa concluída em 106.46932244300842 segundos

Quais são os resultados?

#What are the results?
Resultado:
message: CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH
  success: True
   status: 0
      fun: 11.428966326221078
        x: [ 1.040e-01  3.193e-01]
      nit: 24
      jac: [ 9.160e+00 -1.475e+01]
     nfev: 351
     njev: 117
 hess_inv: <2x2 LbfgsInvHessProduct with dtype=float64>

Parece que tivemos sucesso, o menor erro que conseguimos obter foi 11,43, no entanto, o verdadeiro teste vem quando comparamos o modelo personalizado com o modelo padrão no conjunto de teste.


Testando para Overfitting

Para detectar se estamos sobreajustando os dados de treinamento, vamos comparar os níveis de erro do nosso modelo personalizado com os níveis de erro de um modelo usando configurações padrão. Lembre-se de que dividimos o conjunto de dados em duas metades antes de começarmos o processo de ajuste de parâmetros.
#Now let us compare the default model and the customized model
default_model = SGDRegressor()
customized_model = SGDRegressor(alpha=result.x[0],shuffle=False,eta0=result.x[1])

Primeiro, vamos avaliar os níveis de erro do modelo padrão e do conjunto de teste.

#Default model accuracy
default_model.fit(train_data.loc[:,predictors],merged_df.loc[:(merged_df.shape[0]//2),target])
root_mean_squared_error(merged_df.loc[(merged_df.shape[0]//2):,target],default_model.predict(test_data.loc[:,predictors]))
5.793428451043455

Agora, vamos comparar isso com os níveis de erro do modelo personalizado.

#Customized model accuracy
customized_model.fit(train_data.loc[:,predictors],merged_df.loc[:(merged_df.shape[0]//2),target])
root_mean_squared_error(merged_df.loc[(merged_df.shape[0]//2):,target],customized_model.predict(test_data.loc[:,predictors]))
63.45882351828459

Parece que realmente estávamos sobreajustando os dados de treinamento, e não conseguimos superar as configurações padrão. Neste caso, vamos continuar trabalhando com o modelo padrão e exportá-lo para o formato ONNX.


Exportando para o Formato ONNX

Começamos importando as bibliotecas que precisamos.

#Let's convert the regression model to ONNX format
from skl2onnx.common.data_types import FloatTensorType
from skl2onnx import convert_sklearn
import onnxruntime as ort
import onnx

Em seguida, vamos normalizar e dimensionar nossas entradas.

for i in predictors:
    merged_df.loc[:,i] = (merged_df.loc[:,i] - merged_df.loc[:,i].mean()) / merged_df.loc[:,i].std()

Agora, treine o modelo no conjunto de dados inteiro.

#Prepare the model
model = SGDRegressor()
model.fit(merged_df.loc[:,predictors],merged_df.loc[:,"Target SP500"])

Agora, vamos definir o formato e os tipos de entrada.

#Define the input types
initial_type_float = [("float_input",FloatTensorType([1,len(predictors)]))]
onnx_model_float = convert_sklearn(model,initial_types=initial_type_float,target_opset=12)

Vamos salvar o modelo ONNX.

#ONNX file name
onnx_file_name = "SP500_ONNX_FLOAT_M1.onnx"
#ONNX file
onnx.save_model(onnx_model_float,onnx_file_name)

Agora, vamos inspecionar rapidamente a forma das entradas e saídas do nosso modelo ONNX.

# load the ONNX model and inspect input and ouput shapes
onnx_session = ort.InferenceSession(onnx_file_name)
input_name = onnx_session.get_inputs()[0].name
output_name = onnx_session.get_outputs()[0].name

Vamos garantir que a forma de entrada do nosso modelo seja 1 por 8.

#Display information about input tensors in ONNX
print("Information about input tensors in ONNX:")
for i, input_tensor in enumerate(onnx_session.get_inputs()):
    print(f"{i + 1}. Name: {input_tensor.name}, Data Type: {input_tensor.type}, Shape: {input_tensor.shape}")
Informações sobre os tensores de entrada no ONNX:
1. Nome: float_input, Tipo de Dados: tensor(float), Forma: [1, 8]

Por fim, nossa forma de saída deve ser 1 por 1.

#Display information about output tensors in ONNX
print("Information about output tensors in ONNX:")
for i, output_tensor in enumerate(onnx_session.get_outputs()):
    print(f"{i + 1}. Name: {output_tensor.name}, Data Type: {output_tensor.type}, Shape: {output_tensor.shape}")
Informações sobre os tensores de saída no ONNX:
1. Nome: variable, Tipo de Dados: tensor(float), Forma: [1, 1]

Também podemos visualizar nosso modelo ONNX usando o Netron.

#Visualize the model
import netron

A função start no Netron nos permite visualizar nosso modelo ONNX.

#Call netron 
netron.start(onnx_file_name)

Visualizando nosso modelo ONNX usando o Netron

Fig 11: Visualizando nosso modelo ONNX usando o Netron


Metadados do nosso modelo ONNX

Fig 12: Propriedades do nosso modelo ONNX


Implementação no MQL5

Agora que terminamos de construir nosso modelo ONNX e o exportamos, podemos começar a construir nosso Expert Advisor. A primeira coisa que vamos fazer no nosso Expert Advisor é carregar o modelo ONNX que acabamos de exportar.

//+------------------------------------------------------------------+
//|                                      SP500 X Treasury Yields.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"
#property tester_file "sp500_treasury_yields_scale.csv"

//+------------------------------------------------------------------+
//| Require the ONNX model                                           |
//+------------------------------------------------------------------+
#resource "\\Files\\SP500_ONNX_FLOAT_M1.onnx" as const uchar ModelBuffer[];

A partir daí, também vamos incluir a biblioteca de negociação, que nos ajuda a abrir, fechar e modificar nossas posições.

//+------------------------------------------------------------------+
//| Libraries we need                                                |
//+------------------------------------------------------------------+
#include <Trade/Trade.mqh>
CTrade Trade;

Também é necessário receber algumas entradas do usuário, como qual deve ser o múltiplo do lote e quão largo deve ser o nosso stop loss, depois que isso for feito?

//+------------------------------------------------------------------+
//| Inputs for our EA                                                |
//+------------------------------------------------------------------+
input int lot_multiple = 1; //How many times bigger than minimum lot?
input double sl_width = 1;  //How wide should our stop loss be?

Precisamos de variáveis globais que serão usadas em todo o Expert Advisor. Precisamos de uma variável global para representar o modelo ONNX e outro vetor para armazenar as previsões do nosso modelo.

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
long model;                              //Our ONNX SGDRegressor model
vectorf prediction(1);                   //Our model's prediction
float mean_values[8],variance_values[8]; //We need this data to normalise and scale model inputs
double trading_volume;                   //How big should our positions be?
int state = 0;

Seguindo em frente, também precisamos de uma função responsável por ler o arquivo de configuração CSV que definimos anteriormente. Lembre-se de que esse arquivo é importante porque contém os valores médios e os valores de desvio padrão de cada coluna. Essa função garante que todas as entradas que damos ao nosso modelo ONNX estejam normalizadas. A função começará tentando abrir o arquivo usando o comando de abertura de arquivo. E, se tivermos sucesso e conseguirmos abrir o arquivo, então seguimos para fazer o parsing do nosso arquivo CSV e armazenamos os valores médios e os valores de variância em seus próprios arrays separados. Caso contrário, se não tivermos sucesso, a função irá imprimir que falhou ao ler o arquivo, e retornará falso, fazendo com que o procedimento de inicialização falhe.

//+------------------------------------------------------------------+
//| A function responsible for reading the CSV config file           |
//+------------------------------------------------------------------+
bool read_configuration_file(void)
  {
//--- Read the config file
   Print("Reading in the config file");

//--- Config file name
   string file_name = "sp500_treasury_yields_scale.csv";

//--- Try open the file
   int result = FileOpen(file_name,FILE_READ|FILE_CSV|FILE_ANSI,",");

//--- Check the result
   if(result != INVALID_HANDLE)
     {
      Print("Opened the file");
      //--- Prepare to read the file
      int counter = 0;
      string value = "";
      //--- Make sure we can proceed
      while(!FileIsEnding(result) && !IsStopped())
        {
         if(counter > 60)
            break;
         //--- Read in the file
         value = FileReadString(result);
         Print("Reading: ",value);
         //--- Have we reached the end of the line?
         if(FileIsLineEnding(result))
            Print("row++");
         counter++;
         //--- The first few lines will contain the title of each columns, we will ingore that
         if((counter >= 11) && (counter <= 18))
           {
            mean_values[counter - 11] = (float) value;
           }
         if((counter >= 20) && (counter <= 27))
           {
            variance_values[counter - 20] = (float) value;
           }
        }
      //--- Close the file
      FileClose(result);
      Print("Mean values");
      ArrayPrint(mean_values);
      Print("Variance values");
      ArrayPrint(variance_values);
      return(true);
     }

   else
      if(result == INVALID_HANDLE)
        {
         Print("Failed to read the file");
         return(false);
        }

   return(false);
  }

Também precisamos de uma função responsável por obter uma previsão do nosso modelo. Temos um vetor no início para armazenar os dados de entrada. Uma vez que tivermos todos os preços necessários, subtraímos o valor médio dessa coluna e dividimos pela variância dessa coluna específica. Depois de fazer isso, podemos então obter uma previsão do nosso modelo.

//+------------------------------------------------------------------+
//| A function responsible for getting a forecast from our model     |
//+------------------------------------------------------------------+
void predict(void)
  {
//--- Let's prepare our inputs
   vectorf input_data = vectorf::Zeros(8);
//--- Select the symbol
   input_data[0] = ((iOpen("UST05Y_U4",PERIOD_M1,0) - mean_values[0]) / variance_values[0]);
   input_data[1] = ((iClose("UST05Y_U4",PERIOD_M1,0) - mean_values[1]) / variance_values[1]);
   input_data[2] = ((iHigh("UST05Y_U4",PERIOD_M1,0) - mean_values[2]) / variance_values[2]);
   input_data[3] = ((iLow("UST05Y_U4",PERIOD_M1,0) - mean_values[3]) / variance_values[3]);;
   input_data[4] = ((iOpen("US500",PERIOD_M1,0) - mean_values[4]) / variance_values[4]);;
   input_data[5] = ((iClose("US500",PERIOD_M1,0) - mean_values[5]) / variance_values[5]);;
   input_data[6] = ((iHigh("US500",PERIOD_M1,0) - mean_values[6]) / variance_values[6]);
   input_data[7] = ((iLow("US500",PERIOD_M1,0) - mean_values[7]) / variance_values[7]);;
//--- Show the inputs
   Print("Inputs: ",input_data);
//--- Obtain a prediction from our model
   OnnxRun(model,ONNX_DEFAULT,input_data,prediction);
  }

Após o nosso modelo nos dar uma previsão, precisamos tomar uma ação. Então, neste caso específico, podemos decidir abrir uma posição na direção que nosso modelo previu. Ou, se nosso modelo estiver prevendo que o preço vai reverter contra nós, podemos decidir fechar nossas posições abertas.

//+------------------------------------------------------------------+
//| This function will decide if we should open or close our trades  |
//+------------------------------------------------------------------+
void intepret_prediction(void)
  {
   if(PositionsTotal() == 0)
     {
      double ask = SymbolInfoDouble("US500",SYMBOL_ASK);
      double bid = SymbolInfoDouble("US500",SYMBOL_BID);
      double close = iClose("US500",PERIOD_M1,0);
      if(prediction[0] > close)
        {
         Trade.Buy(trading_volume,"US500",ask,(ask - sl_width),(ask + sl_width),"SP500 X Treasury Yields");
         state = 1;
        }

      if(prediction[0] < iClose("US500",PERIOD_M1,0))
        {
         Trade.Sell(trading_volume,"US500",bid,(bid + sl_width),(bid - sl_width),"SP500 X Treasury Yields");
         state = 2;
        }
     }
   else
      if(PositionsTotal() > 0)
        {
         if((state == 1) && (prediction[0] > iClose("US500",PERIOD_M1,0)))
           {
            Alert("Reversal predicted, consider closing your buy position");
           }

         if((state == 2) && (prediction[0] < iClose("US500",PERIOD_M1,0)))
           {
            Alert("Reversal predicted, consider closing your buy position");
           }
        }

  }

Terminamos de definir as funções auxiliares para o nosso modelo e passamos a definir a função de inicialização do nosso Expert Advisor. Primeiro, precisamos criar nosso modelo ONNX e garantir que o modelo seja válido.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {

//--- Create the ONNX model from the model buffer we have
   model = OnnxCreateFromBuffer(ModelBuffer,ONNX_DEFAULT);

//--- Ensure the model is valid
   if(model == INVALID_HANDLE)
     {
      Comment("[ERROR] Failed to initialize the model: ",GetLastError());
      return(INIT_FAILED);
     }

Uma vez que estamos confiantes de que o modelo é válido, definimos as formas de entrada do nosso modelo e depois definimos as formas de saída do nosso modelo.

//--- Define the model parameters, input and output shapes
   ulong input_shape[] = {1,8};

//--- Check if we were defined the right input shape
   if(!OnnxSetInputShape(model,0,input_shape))
     {
      Comment("[ERROR] Incorrect input shape specified: ",GetLastError(),"\nThe model's inputs are: ",OnnxGetInputCount(model));
      return(INIT_FAILED);
     }

   ulong output_shape[] = {1,1};

//--- Check if we were defined the right output shape
   if(!OnnxSetOutputShape(model,0,output_shape))
     {
      Comment("[ERROR] Incorrect output shape specified: ",GetLastError(),"\nThe model's outputs are: ",OnnxGetOutputCount(model));
      return(INIT_FAILED);
     }

Após tudo isso ser feito, podemos então ler o arquivo de configuração. Isso deve ser feito na inicialização e, se falharmos ao ler o arquivo de configuração, o Expert Advisor inteiro deve ser encerrado, pois não podemos fazer previsões com dados que não estão normalizados.

//--- Read the configuration file
   if(!read_configuration_file())
     {
      Comment("Failed to find the configuration file, ensure it is stored here: ",TerminalInfoString(TERMINAL_DATA_PATH));
      return(INIT_FAILED);
     }

Agora precisamos selecionar os símbolos e adicioná-los ao Market Watch.

//--- Select the symbols
   SymbolSelect("US500",true);
   SymbolSelect("UST05Y_U4",true);

Por fim, precisamos buscar alguns dados de mercado.

//--- Calculate the lotsize
   trading_volume = SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN) * lot_multiple;

//--- Return init succeeded
   return(INIT_SUCCEEDED);
  }
Sempre que nosso Expert Advisor não estiver em uso, devemos liberar os recursos que foram alocados para nós.
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Free up the resources we used for our ONNX model
   OnnxRelease(model);
//--- Remove the expert advisor
   ExpertRemove();
  }

Finalmente, no nosso manipulador de eventos OnTick, faremos previsões usando nosso modelo ONNX e, em seguida, mapearemos essas previsões para ações.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Get a prediction
   predict();
//--- Interpret the forecast
   intepret_prediction();
   Comment("Model forecast",prediction[0]);
  }

Nosso EA em ação

Fig 13: Nosso Expert Advisor em ação.


Conclusão

Neste artigo, revisitamos a estratégia clássica de negociação do SP500 que depende do rendimento das Notas do Tesouro. Nossa análise mostrou que a relação nem sempre é estável e, além disso, parece que os investidores podem estar melhor utilizando dados de mercado ordinários do próprio índice SP500.

Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/15531

Últimos Comentários | Ir para discussão (1)
Maxim Kuznetsov
Maxim Kuznetsov | 17 fev. 2025 em 11:08

A última captura de tela (a mais baixa) tem algo a ver com o artigo e as estratégias mencionadas?

Timeframe M1 e metas de vários pips :-)

Funcionalidades do Assistente MQL5 que você precisa conhecer (Parte 29): Taxas de aprendizado e perceptrons multicamadas Funcionalidades do Assistente MQL5 que você precisa conhecer (Parte 29): Taxas de aprendizado e perceptrons multicamadas
Estamos concluindo a análise da sensibilidade da taxa de aprendizado ao desempenho do EA, estudando taxas de aprendizado adaptáveis Essas taxas devem ser ajustadas para cada parâmetro da camada durante o treinamento, por isso precisamos avaliar os potenciais benefícios em relação às perdas esperadas no desempenho.
Monitoramento de Trading com Notificações-Push — Exemplo de Serviço no MetaTrader 5 Monitoramento de Trading com Notificações-Push — Exemplo de Serviço no MetaTrader 5
Neste artigo, analisaremos a criação de um programa de serviço para enviar notificações para um smartphone sobre os resultados do trading. No decorrer do artigo, aprenderemos a trabalhar com listas de objetos da Biblioteca Padrão para facilitar a seleção de objetos com as propriedades necessárias.
Criando um painel dinâmico multissímbolo e multiperíodo do Índice de Força Relativa (RSI) em MQL5 Criando um painel dinâmico multissímbolo e multiperíodo do Índice de Força Relativa (RSI) em MQL5
Este artigo aborda o desenvolvimento de um painel dinâmico multissímbolo e multiperíodo do indicador RSI em MQL5. O painel tem como objetivo fornecer aos traders os valores do RSI em tempo real para diferentes símbolos e períodos gráficos. Ele será equipado com botões interativos, atualizações em tempo real e indicadores de cores para ajudar os traders a tomarem decisões informadas.
MQL5 Trading Toolkit (Parte 2): Expansão e Aplicação da Biblioteca EX5 para Gerenciamento de Posições MQL5 Trading Toolkit (Parte 2): Expansão e Aplicação da Biblioteca EX5 para Gerenciamento de Posições
Aqui, você aprenderá a importar e utilizar bibliotecas EX5 em seu código ou projetos MQL5. Neste artigo, expandiremos a biblioteca EX5 criada anteriormente, adicionando mais funções de gerenciamento de posições e criando dois Expert Advisors (EA). No primeiro exemplo, usaremos o indicador técnico Variable Index Dynamic Average para desenvolver um EA baseado em uma estratégia de trailing stop. No segundo, implementaremos um painel de negociação para monitorar, abrir, fechar e modificar posições. Esses dois exemplos demonstrarão como utilizar a biblioteca EX5 aprimorada para o gerenciamento de posições.