English Русский Español Deutsch 日本語
preview
Machine Learning e Data Science (Parte 39): Testando a combinação de notícias com IA

Machine Learning e Data Science (Parte 39): Testando a combinação de notícias com IA

MetaTrader 5Experts |
22 0
Omega J Msigwa
Omega J Msigwa

Sumário


Introdução

Você provavelmente já ouviu dizer que os mercados financeiros e cambiais são fortemente influenciados pelas notícias, principalmente pelo Non-Farm Payrolls (NFP). E isso faz sentido, pois as notícias refletem acontecimentos do mundo real.

A negociação baseada em notícias normalmente envolve relatórios econômicos, comunicados corporativos, acontecimentos geopolíticos e declarações de bancos centrais. Quando essas notícias são divulgadas, os instantes imediatamente anteriores ou posteriores à publicação costumam ser acompanhados por maior volatilidade e podem gerar oportunidades de negociação nos ativos e instrumentos relacionados.

Como as notícias descrevem acontecimentos do mundo real em determinada região ou país, bem como seus possíveis efeitos, elas estão entre os melhores preditores do comportamento dos mercados financeiros. Por exemplo, considerando o par EURUSD, uma divulgação do Core CPI da zona do euro indicando alta da inflação pode provocar um movimento de alta do EUR, pois isso geralmente aumenta as expectativas de aperto da política monetária, com elevação das taxas de juros, e também pode contribuir para um movimento de baixa do USD.

Alguns tipos de notícia, como anúncios corporativos e relatórios econômicos, podem afetar os mercados em qualquer direção. Já eventos como desastres naturais tendem, em geral, a exercer um impacto predominantemente negativo sobre os mercados cambial e acionário.

Para nos tornarmos traders bem-sucedidos, não basta considerar apenas os aspectos técnicos do mercado. Também precisamos levar em conta as notícias, pois elas estão entre os principais fatores que influenciam os mercados financeiros.

fonte da imagem: pexels.com

Diante disso, sabemos que as notícias são um dos fatores mais importantes, se não o mais importante, para determinar o comportamento dos mercados. Neste artigo, usaremos as notícias da plataforma MetaTrader 5 como dados de entrada para modelos de IA e veremos se essa combinação realmente é capaz de proporcionar uma vantagem na negociação algorítmica.


Coleta de notícias

Esta é a primeira etapa do nosso projeto.

A coleta de notícias pode ser uma tarefa bastante complexa. É necessário considerar cuidadosamente vários aspectos, entre eles o intervalo de tempo da coleta dos dados, o instrumento utilizado (símbolo) e o tratamento de valores ausentes (NaN).

A seguir, é apresentada a estrutura de dados que contém as variáveis que usaremos para armazenar os dados de notícias.

struct news_data_struct
  {   
    datetime time[];                             //News release time
    string name[];                               //Name of the news
    ENUM_CALENDAR_EVENT_SECTOR sector[];         //The sector a news is related to
    ENUM_CALENDAR_EVENT_IMPORTANCE importance[]; //Event importance
    double actual[];                             //actual value
    double forecast[];                           //forecast value
    double previous[];                           //previous value
  }

Essa estrutura representa parte dos atributos das notícias fornecidos por MqlCalendarEvent e MqlCalendarValue.

A seguir, mostramos como as notícias são coletadas percorrendo os dados históricos.

//--- get OHLC values first
   
   ResetLastError();
   if (CopyRates(Symbol(), timeframe, start_date, end_date, rates)<=0)
     {
       printf("%s failed to get price infromation from %s to %s. Error = %d",__FUNCTION__,string(start_date),string(end_date),GetLastError());
       return;
     }

   MqlCalendarValue values[]; //https://www.mql5.com/en/docs/constants/structures/mqlcalendar#mqlcalendarvalue
   for (uint i=0; i<size-1; i++)
      {         
         int all_news = CalendarValueHistory(values, rates[i].time, rates[i+1].time, NULL, NULL); //we obtain all the news with their values https://www.mql5.com/en/docs/calendar/calendarvaluehistory
         
         for (int n=0; n<all_news; n++)
            {
              MqlCalendarEvent event;
              CalendarEventById(values[n].event_id, event); //Here among all the news we select one after the other by its id https://www.mql5.com/en/docs/calendar/calendareventbyid
                   
              MqlCalendarCountry country; //The couhtry where the currency pair originates
              CalendarCountryById(event.country_id, country); //https://www.mql5.com/en/docs/calendar/calendarcountrybyid
                 
              if (StringFind(Symbol(), country.currency)>-1) //We want to ensure that we filter news that has nothing to do with the base and the quote currency for the current symbol pair
                { 
                     news_data.name[i] = event.name;  
                     news_data.sector[i] = event.sector;
                     news_data.importance[i] = event.importance;
                       
                     news_data.actual[i] = !MathIsValidNumber(values[n].GetActualValue()) ? 0 : values[n].GetActualValue();
                     news_data.forecast[i] = !MathIsValidNumber(values[n].GetForecastValue()) ? 0 : values[n].GetForecastValue();
                     news_data.previous[i] = !MathIsValidNumber(values[n].GetPreviousValue()) ? 0 : values[n].GetPreviousValue();
                }
       }  

Embora esse código permita obter as notícias necessárias, também precisamos coletar os valores de Open, High, Low e Close (OHLC) no momento da publicação de cada notícia. Esses valores serão úteis tanto para a análise quanto para a criação da variável-alvo usada no aprendizado supervisionado.

Também precisamos de uma forma de salvar essas informações em um arquivo CSV para uso posterior fora da plataforma.

A seguir, é apresentada a função completa para a coleta de notícias.

void SaveNews(string csv_name)
 {
//--- get OHLC values first
   
   ResetLastError();
   if (CopyRates(Symbol(), timeframe, start_date, end_date, rates)<=0)
     {
       printf("%s failed to get price infromation from %s to %s. Error = %d",__FUNCTION__,string(start_date),string(end_date),GetLastError());
       return;
     }
      
   uint size = rates.Size();
   news_data.Resize(size-1);

//---

   FileDelete(csv_name); //Delete an existing csv file of a given name
   int csv_handle = FileOpen(csv_name,FILE_WRITE|FILE_SHARE_WRITE|FILE_CSV|FILE_ANSI,",",CP_UTF8); //csv handle
   
   if(csv_handle == INVALID_HANDLE)
     {
       printf("Invalid %s handle Error %d ",csv_name,GetLastError());
       return; //stop the process
     }
     
   FileSeek(csv_handle,0,SEEK_SET); //go to file begining
   FileWrite(csv_handle,"Time,Open,High,Low,Close,Name,Sector,Importance,Actual,Forecast,Previous"); //write csv header
   
   MqlCalendarValue values[]; //https://www.mql5.com/en/docs/constants/structures/mqlcalendar#mqlcalendarvalue
   for (uint i=0; i<size-1; i++)
      {
         news_data.time[i] = rates[i].time;
         news_data.open[i] = rates[i].open;
         news_data.high[i] = rates[i].high;
         news_data.low[i] = rates[i].low;
         news_data.close[i] = rates[i].close;
         
         int all_news = CalendarValueHistory(values, rates[i].time, rates[i+1].time, NULL, NULL); //we obtain all the news with their values https://www.mql5.com/en/docs/calendar/calendarvaluehistory
         
         for (int n=0; n<all_news; n++)
            {
              MqlCalendarEvent event;
              CalendarEventById(values[n].event_id, event); //Here among all the news we select one after the other by its id https://www.mql5.com/en/docs/calendar/calendareventbyid
                   
              MqlCalendarCountry country; //The couhtry where the currency pair originates
              CalendarCountryById(event.country_id, country); //https://www.mql5.com/en/docs/calendar/calendarcountrybyid
                 
              if (StringFind(Symbol(), country.currency)>-1) //We want to ensure that we filter news that has nothing to do with the base and the quote currency for the current symbol pair
                { 
                     news_data.name[i] = event.name;  
                     news_data.sector[i] = event.sector;
                     news_data.importance[i] = event.importance;
                       
                     news_data.actual[i] = !MathIsValidNumber(values[n].GetActualValue()) ? 0 : values[n].GetActualValue();
                     news_data.forecast[i] = !MathIsValidNumber(values[n].GetForecastValue()) ? 0 : values[n].GetForecastValue();
                     news_data.previous[i] = !MathIsValidNumber(values[n].GetPreviousValue()) ? 0 : values[n].GetPreviousValue();
                }
            }
          
          FileWrite(csv_handle,StringFormat("%s,%f,%f,%f,%f,%s,%s,%s,%f,%f,%f",
                                 (string)news_data.time[i],
                                 news_data.open[i],
                                 news_data.high[i],
                                 news_data.low[i],
                                 news_data.close[i],
                                 news_data.name[i],
                                 EnumToString(news_data.sector[i]),
                                 EnumToString(news_data.importance[i]),
                                 news_data.actual[i],
                                 news_data.forecast[i],
                                 news_data.previous[i]
                               ));
       }  
//---

   FileClose(csv_handle);
 }

As informações são salvas no arquivo CSV fora do laço, para que possamos coletar dados tanto das barras em que houve publicação de notícias quanto daquelas em que nenhuma notícia foi divulgada. Isso é importante porque precisamos das barras sem notícias para avaliar o impacto antes e depois de cada publicação.

Escolhi como período de análise o intervalo de 01/01/2023 a 31/12/2023, ou seja, um ano completo de dados de notícias e de negociação.

Escolhi o timeframe de 15 minutos porque, segundo minhas observações, é o timeframe que, segundo minhas observações, a maioria dos traders utiliza no desenvolvimento de filtros de notícias e de estratégias baseadas em notícias de modo geral. Esse timeframe oferece um bom equilíbrio entre capturar reações relevantes dos preços após a publicação das notícias e filtrar o ruído de mercado característico de timeframes inferiores a 15 minutos.


Preparação dos dados de notícias para o treinamento dos modelos

No script Python (Jupyter Notebook), começamos importando o arquivo CSV que contém os dados de notícias.

df = pd.read_csv("/kaggle/input/nfp-forexdata/EURUSD.PERIOD_M15.News.csv")

df.head(5)

Resultados.

Time Open High Low Close Name Sector Importance Actual Forecast Previous
0 2023.01.02 01:00:00 1.06967 1.06976 1.06933 1.06935 New Year's Day CALENDAR_SECTOR_HOLIDAYS CALENDAR_IMPORTANCE_NONE 0.0 0.0 0.0
1 2023.01.02 01:15:00 1.06934 1.06947 1.06927 1.06938 New Year's Day CALENDAR_SECTOR_HOLIDAYS CALENDAR_IMPORTANCE_NONE 0.0 0.0 0.0
2 2023.01.02 01:30:00 1.06939 1.06943 1.06939 1.06942 New Year's Day CALENDAR_SECTOR_HOLIDAYS CALENDAR_IMPORTANCE_NONE 0.0 0.0 0.0
3 2023.01.02 01:45:00 1.06943 1.06983 1.06942 1.06983 New Year's Day CALENDAR_SECTOR_HOLIDAYS CALENDAR_IMPORTANCE_NONE 0.0 0.0 0.0
4 2023.01.02 02:00:00 1.06984 1.06989 1.06984 1.06989 New Year's Day CALENDAR_SECTOR_HOLIDAYS CALENDAR_IMPORTANCE_NONE 0.0 0.0 0.0


Nos dados de notícias, os campos Actual, Previous e Forecast podem conter valores NaN. Por isso, precisamos verificar explicitamente se algum valor NaN foi gravado no arquivo CSV, além das verificações de valores NaN já realizadas no script "Collect News MQL5".

df.info()

Resultados.

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 24848 entries, 0 to 24847
Data columns (total 11 columns):
 #   Column Non-Null Count Dtype  
---  ------      --------------  -----  
 0   Time        24848 non-null  object 
 1   Open        24848 non-null  float64
 2   High        24848 non-null  float64
 3   Low         24848 non-null  float64
 4   Close       24848 non-null  float64
 5   Name        24848 non-null  object 
 6   Sector      24848 non-null  object 
 7   Importance  24848 non-null  object 
 8   Actual      24848 non-null  float64
 9   Forecast    24848 non-null  float64
 10  Previous    24848 non-null  float64
dtypes: float64(7), object(4)
memory usage: 2.1+ MB

Criação da variável-alvo

Em tarefas de aprendizado supervisionado, precisamos de uma variável-alvo que o modelo utilizará para identificar as relações entre os preditores e essa variável.

Sabemos que, após a publicação de uma notícia, os mercados normalmente reagem rapidamente, movendo-se em uma direção ou outra conforme as ações e reações dos traders. A principal dificuldade, porém, está em determinar por quanto tempo o comportamento do mercado ainda pode ser considerado consequência de uma notícia publicada recentemente.

Alguns traders não negociam imediatamente após a divulgação das notícias. Eles podem esperar de 15 a 30 minutos após a publicação, partindo do princípio de que, depois desse intervalo, o impacto da notícia começa a diminuir.

Como o mercado apresenta maior volatilidade após a divulgação de notícias e, às vezes, até picos abruptos de preço, além de uma quantidade significativa de ruído, criaremos a variável-alvo considerando 15 barras à frente, o que corresponde a aproximadamente 4 horas.

lookahead = 15
clean_df = df.copy()

clean_df["Future Close"] = df["Close"].shift(-lookahead)
clean_df.dropna(inplace=True) # drop nan caused by shifting operation

clean_df["Signal"] = (clean_df["Future Close"] > clean_df["Close"]).astype(int) # if the future close > current close = bullish movement otherwise bearish movement

clean_df

Remoção das linhas sem notícias dos dados

Depois de criar a variável-alvo, podemos remover todas as linhas em que nenhuma notícia foi publicada. Precisamos treinar o modelo apenas com as linhas que contêm notícias.

Removemos todas as linhas que apresentam o valor (null) na coluna Name, que contém os nomes das notícias.

clean_df = clean_df[clean_df['Name'] != '(null)']

clean_df

Valores de texto no DataFrame

Como muitos modelos de machine learning não oferecem suporte a valores de texto, precisamos convertê-los em valores inteiros.

Os valores de texto estão presentes nas colunas Name (nome), Sector (setor) e Importance (importância).

from sklearn.preprocessing import LabelEncoder
categorical_cols = ['Name', 'Sector', 'Importance']

label_encoders = {}
encoded_df = clean_df.copy()

for col in categorical_cols:
    le = LabelEncoder()
    encoded_df[col] = le.fit_transform(clean_df[col])

    # Save classes to binary file (.bin)
    with open(f"{col}_classes.bin", 'wb') as f:
        np.save(f, le.classes_, allow_pickle=True)
        
    label_encoders[col] = le  

encoded_df.head(5)

Como alternativa, podemos incluir o LabelEncoder em um Pipeline para simplificar o processamento.

É importante salvar as classes identificadas pelo objeto LabelEncoder para cada coluna codificada, pois precisaremos das mesmas informações ao codificar as notícias nos programas finais escritos em MQL5.

Isso permitirá manter a consistência entre os esquemas e também detectar rapidamente erros caso o codificador encontre notícias que não estavam presentes nos dados de treinamento, já que o surgimento de acontecimentos inesperados no mundo é inevitável.

Resultados.

Time Open High Low Close Name Sector Importance Actual Forecast Previous Future Close Signal
0 2023.01.02 01:00:00 1.06967 1.06976 1.06933 1.06935 162 4 3 0.0 0.0 0.0 1.06880 0
1 2023.01.02 01:15:00 1.06934 1.06947 1.06927 1.06938 162 4 3 0.0 0.0 0.0 1.06888 0
2 2023.01.02 01:30:00 1.06939 1.06943 1.06939 1.06942 162 4 3 0.0 0.0 0.0 1.06891 0
3 2023.01.02 01:45:00 1.06943 1.06983 1.06942 1.06983 162 4 3 0.0 0.0 0.0 1.06892 0
4 2023.01.02 02:00:00 1.06984 1.06989 1.06984 1.06989 162 4 3 0.0 0.0 0.0 1.06897 0


Agora vamos separar os dados em conjuntos X e Y e, em seguida, dividir ambos em conjuntos de treinamento e teste.

X = encoded_df.drop(columns=[
    "Time",
    "Open",
    "High",
    "Low",
    "Close",
    "Future Close",
    "Signal"
])

y = encoded_df["Signal"]

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state = 42, shuffle=True)

Observe que removemos todas as colunas, exceto aquelas que continham informações sobre as notícias.


Treinamento do modelo com dados de notícias

Para o treinamento, escolhi o modelo LightGBM (Light Gradient Boosting Machine), pois é simples, rápido e preciso. Além disso, trata-se de um modelo baseado em árvores de decisão, que apresenta excelente desempenho com dados categóricos como os que temos atualmente.

params = {
    'boosting_type': 'gbdt',  # Gradient Boosting Decision Tree
    'objective': 'binary',  # For binary classification (use 'regression' for regression tasks)
    'metric': ['auc','binary_logloss'],  # Evaluation metric
    'num_leaves': 25,  # Number of leaves in one tree
    'n_estimators' : 100, # number of trees
    'max_depth': 5,
    'learning_rate': 0.05,  # Learning rate
    'feature_fraction': 0.9  # Fraction of features to be used for each boosting round
}

class_weights = compute_class_weight('balanced', classes=np.unique(y_train), y=y_train)
weight_dict = dict(zip(np.unique(y_train), class_weights))

model = lgb.LGBMClassifier(**params, class_weight=weight_dict)

# Fit the model to the training data
model.fit(X_train, y_train)

Aqui foram introduzidos pesos de classe para evitar que as decisões do modelo fiquem enviesadas.

Resultados.

A seguir, é apresentado o relatório de classificação das previsões feitas pelo modelo no conjunto de teste.

[LightGBM] [Warning] feature_fraction is set=0.9, colsample_bytree=1.0 will be ignored. Current value: feature_fraction=0.9
Classification Report
               precision    recall  f1-score   support

           0       0.59      0.52      0.55      1116
           1       0.55      0.61      0.58      1049

    accuracy                           0.56      2165
   macro avg       0.57      0.57      0.56      2165
weighted avg       0.57      0.56      0.56      2165

Uma acurácia geral de 0,56, em uma escala de 0 a 1, é um resultado impressionante nos dados de teste. Alcançar um nível semelhante ao treinar modelos de machine learning exclusivamente com "dados técnicos" não é uma tarefa simples.

Até aqui, o modelo construído funciona como uma caixa-preta, pois não sabemos de que forma as notícias influenciam suas decisões finais. Vamos observar o que o modelo nos revela por meio da importância dos atributos.

Uso do SHAP

import shap

explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_train)

Resultados.

O modelo determina que a coluna Name exerce a maior influência entre todos os atributos, o que significa que determinadas notícias com nomes específicos têm um impacto significativamente maior sobre a reação do mercado do que os demais preditores.

Os valores previstos (Forecast) são o segundo fator mais importante para o modelo, seguidos por Actual, Importance, Previous e Sector da notícia.

Ainda assim, isso não nos dá uma visão completa. Existem várias formas de determinar como cada valor distinto de um atributo influencia o modelo. Uma delas é usar o método SHAP. Por exemplo, podemos avaliar a contribuição da primeira linha dos dados.

i=0
shap.force_plot(explainer.expected_value[1], shap_values[1][i], X_train.iloc[i], matplotlib=True)

Resultados.

Mais informações podem ser encontradas na documentação do SHAPLEY.

Por fim, precisamos salvar o modelo no formato ONNX para uso externo.

# Registering ONNX converter

update_registered_converter(
    lgb.LGBMClassifier,
    "GBMClassifier",
    calculate_linear_classifier_output_shapes,
    convert_lightgbm,
    options={"nocl": [False], "zipmap": [True, False, "columns"]},
)

# Final conversion

model_onnx = convert_sklearn(
    model,
    "lightgbm_model",
    [("input", FloatTensorType([None, X_train.shape[1]]))],
    target_opset={"": 14, "ai.onnx.ml": 2},
)

# And save.
with open("lightgbm.EURUSD.news.M15.onnx", "wb") as f:
    f.write(model_onnx.SerializeToString())


Robô de negociação: Notícias + Inteligência Artificial

Para que esse robô de negociação funcione, precisamos adicionar algumas dependências e arquivos.

#define NEWS_CSV "EURUSD.PERIOD_M15.News.csv"         //For simulating news on the strategy tester, making testing possible
//--- Encoded classes for the columns stored in a binary file
#define SECTOR_CLASSES "Sector_classes.bin"
#define NAME_CLASSES "Name_classes.bin"
#define IMPORTANCE_CLASSES "Importance_classes.bin"
#define LIGHTGBM_MODEL "lightgbm.EURUSD.news.M15.onnx" //AI model 

//--- Tester files

#property  tester_file NEWS_CSV
#property  tester_file SECTOR_CLASSES 
#property  tester_file NAME_CLASSES
#property  tester_file IMPORTANCE_CLASSES 
#property  tester_file LIGHTGBM_MODEL

Também precisamos permitir o uso desses arquivos no testador de estratégias, pois é justamente nesse ambiente que eles serão mais necessários.

//--- Dependencies

#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
#include <pandas.mqh>   //https://www.mql5.com/en/articles/17030
#include <Lightgbm.mqh> //For importing LightGBM model

CLightGBMClassifier lgbm;
CTrade m_trade;
CPositionInfo m_position;

Precisamos da mesma estrutura de notícias utilizada em Collect News.mq5, o script empregado na coleta dos dados de notícias.

MqlRates rates[];
struct news_data_struct
  {   
    datetime time;
    double open;
    double high;
    double low;
    double close;
    int name;
    int sector;
    int importance;
    double actual;
    double forecast;
    double previous;
    
  } news_data;      

Como o MQL5 possui um equivalente ao LabelEncoder, semelhante ao que usamos em Python para converter atributos de texto, podemos carregar suas classes e atribuí-las a três variáveis, uma para cada coluna: Name, Sector e Importance.

CLabelEncoder le_name,
              le_sector,
              le_importance;

A função Init deve ser praticamente à prova de falhas: o EA só deve poder ser inicializado se todos os arquivos tiverem sido importados e carregados com sucesso e seus dados atribuídos aos respectivos arrays e objetos.

CDataFrame news_df; //Pandas like Dataframe object from pandas.mqh
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
  
//--- Initializing LightGBM model

   if (!lgbm.Init(LIGHTGBM_MODEL, ONNX_DEFAULT))
      {
         printf("%s failed to initialize ONNX model, error = %d",__FUNCTION__,GetLastError());
         return INIT_FAILED;
      }
      
//--- Assign the classes read from Binary files to the label encoders class objects

   if (!read_bin(le_name.m_classes, NAME_CLASSES))
      {
         printf("%s Failed to read name classes for the news, Error = %d",__FUNCTION__,GetLastError());
         return INIT_FAILED;
      }
      
   if (!read_bin(le_sector.m_classes, SECTOR_CLASSES))
      {
         printf("%s Failed to read sector classes for the news, Error = %d",__FUNCTION__,GetLastError());
         return INIT_FAILED;
      }
      
   if (!read_bin(le_importance.m_classes, IMPORTANCE_CLASSES))
      {
         printf("%s Failed to read importance classes for the news, Error = %d",__FUNCTION__,GetLastError());
         return INIT_FAILED;
      }
      
//--- Setting the symbol and timeframe
   
   if (!MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_DEBUG))
     if (!ChartSetSymbolPeriod(0, symbol_, timeframe))
       {
         printf("%s failed to set symbol %s and timeframe %s",__FUNCTION__,symbol_,EnumToString(timeframe));
         return INIT_FAILED;
       }

//--- Loading news from a csv file for testing the EA in the strategy tester
   
   if (MQLInfoInteger(MQL_TESTER))
      {
         if (!news_df.from_csv(NEWS_CSV,",",
                               false,
                               "Time",
                               "Name,Sector,Importance"
                               ))
            {
               printf("%s failed to read news from a file %s, Error = %d",__FUNCTION__,NEWS_CSV,GetLastError());
               return INIT_FAILED;
            }   
      }
   
//--- Configuring the CTrade class

   m_trade.SetExpertMagicNumber(magic_number);
   m_trade.SetDeviationInPoints(slippage);
   m_trade.SetMarginMode();
   m_trade.SetTypeFillingBySymbol(Symbol());

   return(INIT_SUCCEEDED);
  }

A função from_csv, disponibilizada por CDataFrame, codifica automaticamente os valores datetime e as colunas de texto quando configurada para isso.

bool CDataFrame::from_csv(string file_name,string delimiter=",",bool is_common=false, string datetime_columns="",string encode_columns="", bool verbosity=false)

Isso simplifica o processamento dos dados armazenados no objeto news_df, pois não precisaremos codificar manualmente as colunas extraídas do arquivo CSV.

A coluna Time será convertida em segundos e armazenada como double, em vez de permanecer no tipo datetime.

Os dados obtidos são mostrados a seguir.

news_df.head();

Resultados.

QE      0       18:21:45.159    Core 1  2023.01.01 00:00:00   | Index | Time                    | Open           | High           | Low            | Close          | Name             | Sector         | Importance     | Actual         | Forecast       | Previous       |
MI      0       18:21:45.159    Core 1  2023.01.01 00:00:00   |     0 | 1672621200.00000000     | 1.06967000     | 1.06976000     | 1.06933000     | 1.06935000     | 161.00000000     | 4.00000000     | 3.00000000     | 0.00000000     | 0.00000000     | 0.00000000     |
JI      0       18:21:45.159    Core 1  2023.01.01 00:00:00   |     1 | 1672622100.00000000     | 1.06934000     | 1.06947000     | 1.06927000     | 1.06938000     | 161.00000000     | 4.00000000     | 3.00000000     | 0.00000000     | 0.00000000     | 0.00000000     |
RI      0       18:21:45.159    Core 1  2023.01.01 00:00:00   |     2 | 1672623000.00000000     | 1.06939000     | 1.06943000     | 1.06939000     | 1.06942000     | 161.00000000     | 4.00000000     | 3.00000000     | 0.00000000     | 0.00000000     | 0.00000000     |
JI      0       18:21:45.159    Core 1  2023.01.01 00:00:00   |     3 | 1672623900.00000000     | 1.06943000     | 1.06983000     | 1.06942000     | 1.06983000     | 161.00000000     | 4.00000000     | 3.00000000     | 0.00000000     | 0.00000000     | 0.00000000     |
JI      0       18:21:45.159    Core 1  2023.01.01 00:00:00   |     4 | 1672624800.00000000     | 1.06984000     | 1.06989000     | 1.06984000     | 1.06989000     | 161.00000000     | 4.00000000     | 3.00000000     | 0.00000000     | 0.00000000     | 0.00000000     |

É dentro da função getNews que ocorre a maior parte dos cálculos.

vector getNews()
 { 
//---

   vector v = vector::Zeros(6);
   ResetLastError();
   if (CopyRates(Symbol(), timeframe, 0, 1, rates)<=0)
     {
       printf("%s failed to get price infromation. Error = %d",__FUNCTION__,GetLastError());
       return vector::Zeros(0);
     }
   
   news_data.time = rates[0].time;
   news_data.open = rates[0].open;
   news_data.high = rates[0].high;
   news_data.low = rates[0].low;
   news_data.close = rates[0].close;
   
//---
   
   if (MQLInfoInteger(MQL_TESTER)) //If we are on the strategy tester, read the news from a dataframe object
    {
      if ((ulong)n_idx>=news_df["Time"].Size())
        TesterStop(); //End the strategy tester as there are no enough news to read
      
      datetime news_time = (datetime)news_df["Time"][n_idx]; //Convert time from seconds back into datetime
      datetime current_time = TimeCurrent();
      
      if (news_time >= (current_time - PeriodSeconds(timeframe)) && 
         (news_time <= (current_time + PeriodSeconds(timeframe)))) //We ensure if the incremented news time is very close to the current time
        {
          n_idx++; //Move on to the next news if weve passed the previous one
        }
      else
         return vector::Zeros(0);
      
      if (n_idx>=(int)news_df["Name"].Size() || n_idx >= (int)news_df.m_values.Rows())
        TesterStop(); //End the strategy tester as there are no enough news to read
      
      news_data.name = (int)news_df["Name"][n_idx]; 
      news_data.sector = (int)news_df["Sector"][n_idx];
      news_data.importance = (int)news_df["Importance"][n_idx];
        
      news_data.actual = !MathIsValidNumber(news_df["Actual"][n_idx]) ? 0 : news_df["Actual"][n_idx];
      news_data.forecast = !MathIsValidNumber(news_df["Forecast"][n_idx]) ? 0 : news_df["Forecast"][n_idx];
      news_data.previous = !MathIsValidNumber(news_df["Previous"][n_idx]) ? 0 : news_df["Previous"][n_idx];
      
      if (news_data.name==0.0) //(null)
         return vector::Zeros(0);
    }
   else
    {
      int all_news = CalendarValueHistory(calendar_values, rates[0].time, rates[0].time+PeriodSeconds(timeframe), NULL, NULL); //we obtain all the news with their calendar_values https://www.mql5.com/en/docs/calendar/calendarvaluehistory
      
      if (all_news<=0)
         return vector::Zeros(0);
      
      for (int n=0; n<all_news; n++)
         {
           MqlCalendarEvent event;
           CalendarEventById(calendar_values[n].event_id, event); //Here among all the news we select one after the other by its id https://www.mql5.com/en/docs/calendar/calendareventbyid
                
           MqlCalendarCountry country; //The couhtry where the currency pair originates
           CalendarCountryById(event.country_id, country); //https://www.mql5.com/en/docs/calendar/calendarcountrybyid
              
           if (StringFind(Symbol(), country.currency)>-1) //We want to ensure that we filter news that has nothing to do with the base and the quote currency for the current symbol pair
             { 
		//--- Important | Encode news names into integers using the same encoder applied on the training data

                  news_data.name = le_name.transform((string)event.name);  
                  news_data.sector = le_sector.transform((string)event.sector);
                  news_data.importance = le_importance.transform((string)event.importance);
                    
                  news_data.actual = !MathIsValidNumber(calendar_values[n].GetActualValue()) ? 0 : calendar_values[n].GetActualValue();
                  news_data.forecast = !MathIsValidNumber(calendar_values[n].GetForecastValue()) ? 0 : calendar_values[n].GetForecastValue();
                  news_data.previous = !MathIsValidNumber(calendar_values[n].GetPreviousValue()) ? 0 : calendar_values[n].GetPreviousValue();
             }
         } 
         
      if (news_data.name==0.0) //(null)
         return vector::Zeros(0);
    }
    
   v[0] = news_data.name;
   v[1] = news_data.sector;
   v[2] = news_data.importance;
   v[3] = news_data.actual;
   v[4] = news_data.forecast;
   v[5] = news_data.previous;      
   
   return v;
 }

Quando essa função detecta que o EA está sendo executado no testador de estratégias, ela lê as notícias armazenadas no objeto DataFrame em vez de tentar obtê-las diretamente do mercado, o que não é possível no ambiente do testador.

Observe que as strings extraídas das notícias foram convertidas em valores inteiros usando os codificadores preenchidos com as mesmas classes utilizadas durante o treinamento do modelo e carregadas na função OnInit.

Como a função getNews inclui várias verificações que garantem o retorno de um vetor vazio em caso de erro ou quando não há notícias no momento atual, na função OnTick verificamos se o vetor retornado não está vazio. Se houver dados, passamos à execução de uma estratégia de negociação simples.

void OnTick()
  {
//---   
   vector x = getNews();
   
   if (x.Size()==0) //No present news at the moment
      return; 
   
   long signal = lgbm.predict(x).cls;
   
//---
      
   MqlTick ticks;
   if (!SymbolInfoTick(Symbol(), ticks))
      {
         printf("Failed to obtain ticks information, Error = %d",GetLastError());
         return;
      }
      
   double volume_ = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);
   
//---
   
   if (signal == 1) //Check if there are is atleast a special pattern before opening a trade
     {        
        if (!PosExists(POSITION_TYPE_BUY) && !PosExists(POSITION_TYPE_SELL))  
            m_trade.Buy(volume_, Symbol(), ticks.ask,0,0);
     }
     
   if (signal == 0) //Check if there are is atleast a special pattern before opening a trade
     {        
        if (!PosExists(POSITION_TYPE_SELL) && !PosExists(POSITION_TYPE_BUY))  
            m_trade.Sell(volume_, Symbol(), ticks.bid,0,0);
     } 
    
    CloseTradeAfterTime((Timeframe2Minutes(Period())*lookahead)*60); //Close the trade after a certain lookahead and according the the trained timeframe
  }

Se o modelo prever, com base nas notícias recebidas, que o mercado irá subir (signal = 1), abrimos uma posição de compra. Se o modelo prever uma queda do mercado (signal = 0), abrimos uma posição de venda.

A posição será encerrada depois de transcorrido um número de barras igual ao valor de lookahead no timeframe atual. O valor de lookahead deve coincidir com o utilizado na criação da variável-alvo no script Python. Isso significa que as posições serão mantidas de acordo com o horizonte de previsão do modelo treinado.

Por fim, vamos testar o EA no testador de estratégias do MetaTrader 5 durante o mesmo período utilizado no treinamento.

  • Instrumento: EURUSD
  • Timeframe: PERIOD_M15
  • Modo de modelagem: apenas preços de abertura

Resultado do testador de estratégias.


Conclusão

Como podemos ver pelos resultados do testador, a combinação de notícias com um modelo de machine learning de alto desempenho, neste caso o LightGBM, apresentou resultados impressionantes tanto em termos de previsão quanto de negociação no mesmo ano utilizado para o treinamento.

Embora as notícias estejam entre os preditores mais relevantes dos mercados cambial e acionário, negociar durante sua divulgação ou imediatamente depois envolve um risco muito elevado devido à volatilidade imprevisível que surge nesse período. É importante ter isso em mente sempre que você considerar confiar seu dinheiro, conquistado com esforço, a um robô que negocia com base em notícias.

Podemos afirmar com segurança que este projeto ainda tem potencial para ser aprimorado. Portanto, experimente diferentes parâmetros e desenvolva a ideia. Compartilhe suas opiniões na seção de discussão.

Boa sorte a todos!

Continue conosco e contribua para o desenvolvimento de algoritmos de machine learning para a linguagem MQL5 neste repositório do GitHub.



Tabela de anexos

Nome e caminho do arquivo
Descrição e finalidade


Files\AI+NFP.mq5

EA principal para integrar modelos de IA e notícias à negociação e aos testes.

Files\Collect News.mq5

Script para coletar notícias do MetaTrader 5 e exportá-las para um arquivo CSV.

Include\Lightgbm.mqh

Biblioteca para carregar e implantar o modelo LightGBM no formato ONNX.

Include\pandas.mqh

Biblioteca que contém um DataFrame semelhante ao Pandas para armazenamento e processamento de dados.

Files\*

Os arquivos ONNX, CSV e binários utilizados neste artigo estão localizados nesta pasta.

Python\nfp-ai.ipynb

Jupyter Notebook que reúne todo o código Python utilizado para treinamento, limpeza dos dados etc.

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

Arquivos anexados |
Attachments.zip (477.84 KB)
Machine Learning e Data Science (Parte 40): Uso dos níveis de Fibonacci em dados para machine learning Machine Learning e Data Science (Parte 40): Uso dos níveis de Fibonacci em dados para machine learning
Os níveis de retração de Fibonacci são uma ferramenta popular na análise técnica. Eles são usados para identificar possíveis zonas de reversão. Neste artigo, veremos como esses níveis de retração podem ser transformados em variáveis-alvo para modelos de machine learning, ajudando-os a compreender melhor o mercado.
Trading com o Calendário Econômico MQL5 (Parte 8): Otimizando o teste de estratégias baseadas em notícias com filtros e logs Trading com o Calendário Econômico MQL5 (Parte 8): Otimizando o teste de estratégias baseadas em notícias com filtros e logs
Neste artigo, vamos otimizar nosso calendário econômico adicionando uma filtragem inteligente de eventos e logs para tornar os testes de estratégias nos modos live e offline mais rápidos e intuitivos. Vamos otimizar o processamento dos eventos e registrar no log apenas as operações realmente importantes e os eventos relevantes do painel. Também tentaremos melhorar a visualização da estratégia. Todas essas melhorias devem facilitar o teste e o aprimoramento de estratégias de trading baseadas em notícias.
Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
Desenvolvimento de ferramentas para análise de Price Action (Parte 22): Painel de correlação Desenvolvimento de ferramentas para análise de Price Action (Parte 22): Painel de correlação
Esta ferramenta consiste em um painel de correlações que calcula e exibe, em tempo real, os coeficientes de correlação de vários pares de moedas. Ao mostrar como os pares se movimentam em relação uns aos outros, a ferramenta acrescenta um contexto importante à análise de Price Action e ajuda a compreender melhor as relações entre diferentes mercados. Vamos analisar seus recursos e possíveis aplicações.