English Deutsch 日本語
preview
Машинное обучение и Data Science (Часть 39): Тестируем связку новостей с ИИ

Машинное обучение и Data Science (Часть 39): Тестируем связку новостей с ИИ

MetaTrader 5Эксперты |
59 0
Omega J Msigwa
Omega J Msigwa

Содержание


Введение

Наверняка вы слышали, что финансовые и валютные рынки движимы и подвержены влиянию новостей, особенно Non-Farm Payrolls (NFP). Это так, ведь новости описывают текущие события в реальном мире.

Торговля на новостях обычно включает экономические отчеты, корпоративные заявления, геополитические события и сообщения от центральных банков. Часто, когда выходят такие новости (за несколько мгновений до или после), они создают волатильность и торговые возможности по связанным активам и инструментам.

Поскольку новости описывают происходящее в мире в конкретном регионе или стране и их ожидаемые последствия, они являются одними из лучших предикторов финансовых рынков. Например, если брать пару EURUSD, то информация о росте CPI в новости Core CPI по евро может привести к бычьему движению EUR, поскольку это часто повышает ожидания ужесточения монетарной политики (повышения процентных ставок), а также может привести к медвежьему движению USD. 

Есть определенный тип новостей, например корпоративные объявления и экономические отчеты, которые могут оказывать влияние на рынки в любом направлении. А есть события, такие как стихийные бедствия, которые, как правило, оказывают преимущественно негативное влияние на валютные и фондовые рынки.

Чтобы стать успешными трейдерами, недостаточно полагаться исключительно на технические аспекты рынка. Нужно также учитывать новости, поскольку они являются одним из ключевых факторов, влияющих на финансовые рынки.

источник изображения: pexels.com

Учитывая сказанное, мы знаем, что новости являются одним из, если не самым значимым, факторов, определяющих поведение рынков. В этой статье мы будем новости из платформы MetaTrader 5 в качестве входных данных для AI-моделей и посмотрим, действительно ли такое сочетание способно обеспечить преимущество в алгоритмической торговле.


Сбор новостей

Это первый этап, который нам предстоит пройти в рамках нашего проекта.

Сбор новостей может быть достаточно сложной задачей. Здесь необходимо тщательно учитывать несколько моментов, включая временные рамки сбора данных, используемый инструмент (символ) и обработку пустых переменных (NaN).

Ниже представлена структура данных, содержащая переменные, которые мы будем использовать для хранения собираемых новостных данных.

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
  }

Данная структура отражает часть атрибутов новостей, предоставляемых MqlCalendarEvent и MqlCalendarValue.

Ниже показано, как осуществляется сбор новостей путем перебора исторических данных.

//--- 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();
                }
       }  

Хотя данный код позволяет получить необходимые новости, нам также требуется собрать значения Open, High, Low и Close (OHLC) на момент публикации новости. Эти значения будут полезны при анализе и при формировании целевой переменной для обучения с учителем.

Также нам нужен функционал для сохранения этой информации в CSV-файл для последующего внешнего использования.

Ниже приведена полная функция для сбора новостей.

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);
 }

Процесс сохранения информации в CSV-файл выполняется вне цикла, чтобы мы могли собирать данные как для баров, на которых новости публиковались, так и для тех, на которых новостей не было. Это важно, поскольку нам нужны бары без новостей для оценки влияния новостей до и после их публикации.

В качестве периода я выбрал с 01.01.2023 по 31.12.2023, то есть полный год новостных и торговых данных

Я выбрал таймфрейм 15 минут, поскольку именно его, по моим наблюдениям, большинство трейдеров используют при разработке новостных фильтров и в целом новостных стратегий. Это оптимальный баланс между захватом значимых ценовых реакций после публикации новостей и фильтрацией рыночного шума, который характерен для таймфреймов ниже 15 минут.


Подготовка новостных данных для обучения моделей

Внутри Python-скрипта (Jupyter Notebook) мы начинаем с импорта CSV-файла, содержащего новостные данные.

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

df.head(5)

Результаты.

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


Для новостных данных в полях Факт, Предыдущее и Прогноз используются значения NaN. Поэтому необходимо явно проверять, не попали ли в CSV-файл какие-либо значения NaN, дополнительно к проверкам на отсутствие NaN внутри скрипта 'Collect News MQL5'.

df.info()

Результаты.

<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

Формирование целевой переменной

В задачах обучения с учителем необходима целевая переменная, которую модель будет использовать для установления взаимосвязей между предикторами и этой целевой переменной.

Мы знаем, что после публикации новости рынки обычно быстро реагируют, двигаясь в том или ином направлении в зависимости от действий и реакций трейдеров. Однако основная сложность заключается в определении того, как долго происходящее на рынке можно считать следствием недавно опубликованной новости.

Есть те, кто не торгует непосредственно сразу после выхода новостей, они могут ждать 15–30 минут после публикации, полагая, что по истечении этого времени влияние новости ослабевает.

Поскольку после выхода новостей рынок испытывает повышенную волатильность и даже иногда шпильки, со значительным количеством шума, мы создадим целевую переменную на 15 баров вперед (примерно на 4 часа вперед по времени).

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

Удаление строк без новостей из данных

После формирования целевой переменной мы можем удалить все строки, в которых новости не публиковались. Нам нужно обучать модель только на строках, содержащих новости.

Фильтруем все строки со значением (null) в столбце Name (столбец, содержащий названия новостей).

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

clean_df

Строковые значения в датафрейме

Поскольку строковые значения не поддерживаются во многих моделях машинного обучения, нужно преобразовать строковые значения в integer.

Строковые значения присутствуют в столбцах: Name (название), Sector (сектор) и Importance (важность).

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)

В качестве альтернативы можно обернуть LabelEncoder в Pipeline для удобства.

Важно сохранить классы, которые объект label encoder нашел для каждого запрограммированного столбца, поскольку нам потребуется та же информация при кодировании новостей в финальных программах, написанных на языке MQL5.

Это позволит соблюсти согласованность схем, а также для своевременно выявлять ошибки в случае, если энкодер сталкивается с новостями, на которых он не обучался, поскольку появление неожиданных новостей в мире неизбежно.

Результаты.

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


Теперь разделим данные на выборки X и Y, а затем разделим эти два набора на обучающую и тестовую выборки.

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)

Обратите внимание, что мы удалили все столбцы, кроме тех, которые содержали информацию о новостях.


Обучение модели на новостных данных

Для обучения я выбрал модель LightGBM (Light Gradient Boosting Machine), поскольку она простая, быстрая и точная. Не говоря уже о том, что это модель, основанная на деревьях решений, которая отлично работает с категориальными данными, подобными тем, которые у нас сейчас есть.

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)

Здесь введены весовые коэффициенты классов для предотвращения предвзятости в решениях моделей.

Результаты.

Ниже представлен отчет о классификации прогнозов, сделанных моделью на тестовой выборке.

[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

Общая точность 0.56 из 1.0 на тестовых данных является впечатляющим результатом. Достичь подобного уровня при обучении моделей машинного обучения исключительно на "технических данных" непросто.

На данный момент построенная модель представляет собой черный ящик — мы не знаем, каким образом новости влияют на итоговые решения модели. Посмотрим, какую историю модель рассказывает нам через важность признаков.

Использование SHAP

import shap

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

Результаты.

Модель определяет, что столбец Name оказывает наибольшее влияние среди всех признаков, что означает: некоторые новости с конкретными названиями оказывают значительно более сильное влияние на рыночную реакцию по сравнению с другими предикторами.

Прогнозируемые значения (Forecast) являются вторым по значимости фактором для модели, за ними следуют Actual, Importance, Previous и Sector новости.

Тем не менее это по-прежнему не дает полной ясности. Существует множество способов определить влияние каждого уникального значения внутри признака на модель, например, с использованием метода SHAP — в частности, можно оценить вклад первой строки данных.

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

Результаты.

Больше информации можете найти в документации SHAPLEY.

Наконец, нам необходимо сохранить модель в формате ONNX для внешнего использования.

# 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())


Торговый робот: Новости + Искусственный интеллект

Чтобы этот торговый робот работал, нужно добавить несколько зависимостей и файлов.

#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

Мы должны разрешить использование этих файлов в тестере стратегий, поскольку именно там они наиболее необходимы.

//--- 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;

Нужна та же структура новостей, что и использовалась в Collect News.mq5 (скрипт, применявшийся для сбора новостных данных).

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;      

Поскольку в MQL5 имеется аналог LabelEncoder, схожий с тем, который мы использовали в Python для преобразования строковых признаков, мы можем загрузить его классы и назначить их трем переменным — по одной для каждого столбца (Name, Sector и Importance).

CLabelEncoder le_name,
              le_sector,
              le_importance;

Функция Init должна быть практически безупречной: роботу надо разрешить инициализироваться только в том случае, если все файлы успешно импортированы, загружены и присвоены соответствующим массивам и объектам.

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);
  }

Функция from_csv, предоставляемая CDataFrame, автоматически кодирует значения datetime и строковые столбцы при соответствующей настройке.

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

Это упрощает работу с полученными данными, хранящимися в объекте news_df, поскольку нам не нудно будет вручную кодировать столбцы, извлеченные из CSV-файла.

Столбец Time будет преобразован в секунды (тип данных double) вместо datetime (тип данных datetime).

Полученные данные показаны ниже.

news_df.head();

Результаты.

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     |

Именно внутри функции getNews выполняется основной объем вычислений.

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;
 }

Когда данная функция видит, что советник запущен в тестере стратегий, она считывает новости, хранимые в объекте DataFrame, а не пыьается получить их непосредственно с рынка (это невозможно в среде тестера).

Обратите внимание, что строки, полученные из новостей, были преобразованы в целые числа с использованием энкодеров, заполненных классами, применявшимися при обучении данных внутри функции OnInit.

Поскольку внутри функции getNews предусмотрено несколько проверок, гарантирующих возврат пустого вектора при возникновении ошибки или при отсутствии новостей в текущий момент, в функции OnTick мы проверяем, что полученный вектор не пуст. Если он не пуст, переходим к выполнению простой торговой стратегии.

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
  }

Если модель прогнозирует, что на основании полученных новостей рынок будет расти (signal = 1), мы открываем позицию на покупку; если модель прогнозирует снижение рынка (signal = 0), открываем позицию на продажу.

Сделка будет закрыта после прохождения количества баров, равного значению lookahead на текущем таймфрейме. Значение lookahead должно совпадать с тем, которое использовалось при формировании целевой переменной в Python-скрипте. Это значит, что позиции будут удерживаться в соответствии с горизонтом прогнозирования обученной модели.

Наконец, протестируем советник в тестере стратегий MetaTrader 5 на том же периоде, на котором он обучался.

  • Инструмент: EURUSD
  • Таймфрейм: PERIOD_M15
  • Режим моделирования: только цены открытия

Результат из тестера стратегий.


Заключение

Как видно из результатов тестера, сочетание новостей и производительной модели машинного обучения (в данном случае LightGBM) продемонстрировало впечатляющие прогнозные и торговые результаты на том году, на котором проводилось обучение.

Хотя новости являются одним из самых сильных предикторов валютных и фондовых рынков, торговля во время выхода новостей или сразу после них сопряжена с очень высоким риском из-за непредсказуемой волатильности, возникающей в этот период. Об этом нужно помнить каждый раз, когда вы собираетесь доверить свои с трудом заработанные средства роботу, торгующему на новостях.

Можно с уверенностью сказать, что у данного проекта есть потенциал для улучшения. Поэтому поэкспериментируйте с параметрами и развивайте идею. Делитесь своими мыслями в разделе обсуждения.

Всем удачи!

Оставайтесь с нами и вносите свой вклад в разработку алгоритмов машинного обучения для языка MQL5 в этом GitHub-репозитории.



Таблица вложений

Имя файла и путь
Описание и назначение


Files\AI+NFP.mq5

Основной советник по внедрению моделей ИИ и новостей для торговли и тестирования.

Files\Collect News.mq5

Скрипт для сбора новостей из MetaTrader 5 и экспорта их в CSV-файл.

Include\Lightgbm.mqh

Библиотека для загрузки и развертывания модели LightGBM в формате ONNX.

Include\pandas.mqh

Библиотека, содержащая DataFrame, аналогичный Pandas, для хранения и обработки данных.

Files\*

Файлы ONNX, CSV и бинарные файлы, использованные в этой статье, находятся в этой папке.

Python\nfp-ai.ipynb

Jupyter Notebook — это хранилище всего кода Python, используемого для обучения, очистки данных и т.д.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17986

Прикрепленные файлы |
Attachments.zip (477.84 KB)
Торговля LLM-агента со встроенной философией топ-трейдеров Торговля LLM-агента со встроенной философией топ-трейдеров
В работе проведен критический разбор LLM-стратегии, где прогноз направления отделен от торговых решений, и показано, почему это ведет к разрыву между метриками и PnL. Описаны процедуры балансировки датасета, инженерии признаков, подготовки промптов и ответов, настройки файнтюнинга в Ollama и надежного парсинга. Бэктест и форвард-тест выявляют систематическую деградацию. Практический вывод — необходимость формулировать задачу как прямую оптимизацию торговых исходов.
Торговые инструменты на MQL5 (Часть 1): Интерактивный визуальный помощник для работы с отложенными ордерами Торговые инструменты на MQL5 (Часть 1): Интерактивный визуальный помощник для работы с отложенными ордерами
В этой статье мы представляем разработку интерактивного инструмента Trade Assistant Tool на языке MQL5, предназначенного для упрощения размещения отложенных ордеров на рынке Форекс. В статье описан концептуальный дизайн. Особое внимание уделено удобному графическому интерфейсу пользователя для визуальной установки уровней входа, стоп-лосса и тейк-профита на графике. Кроме того, мы подробно описываем реализацию на MQL5 и тестирование на истории для обеспечения надежности инструмента, что подготавливает почву для введения расширенных функций в последующих частях серии.
Возможности Мастера MQL5, которые вам нужно знать (Часть 63): Использование паттернов каналов Демарка и конвертов Возможности Мастера MQL5, которые вам нужно знать (Часть 63): Использование паттернов каналов Демарка и конвертов
Осциллятор Демарка (DeMarker Oscillator) и конверты (Envelopes) — это инструменты, определяющие импульс и уровни поддержки/сопротивления, которые можно использовать в паре при разработке советника. Поэтому мы рассматриваем каждый паттерн отдельно, чтобы определить, что может быть полезным, а чего потенциально следует избегать. Как всегда, мы используем советник, собранный в Мастере, вместе с функциями паттернов, встроенными в класс сигналов советника (Expert Signal).
Как использовать конечные разности для прогнозирования цен Как использовать конечные разности для прогнозирования цен
Рассматривается практическое использование конечных разностей в трейдинге: типы разностей, их связь с динамикой цены и биноминальное преобразование для фильтрации шумов. Описаны правила кодирования паттернов по уровням разностей и применение этих паттернов к прогнозу. Приведены наивные, адаптивные и вероятностные подходы, которые помогают сглаживать ряды, выделять повторяющиеся структуры и оценивать будущие движения.