Machine Learning e Data Science (Parte 39): Testando a combinação de notícias com IA
Sumário
- Introdução
- Coleta de notícias
- Preparação dos dados de notícias para o treinamento dos modelos
- Treinamento do modelo com dados de notícias
- Robô de negociação: Notícias + Inteligência Artificial
- Conclusão
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.
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.
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
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Machine Learning e Data Science (Parte 40): Uso dos níveis de Fibonacci em dados para machine learning
Trading com o Calendário Econômico MQL5 (Parte 8): Otimizando o teste de estratégias baseadas em notícias com filtros e logs
Está chegando o novo MetaTrader 5 e MQL5
Desenvolvimento de ferramentas para análise de Price Action (Parte 22): Painel de correlação
- 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




