English Deutsch 日本語
preview
Машинное обучение и Data Science (Часть 36): Работа с несбалансированными финансовыми рынками

Машинное обучение и Data Science (Часть 36): Работа с несбалансированными финансовыми рынками

MetaTrader 5Торговые системы |
87 3
Omega J Msigwa
Omega J Msigwa

Содержание


Введение

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

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

Давайте возьмем пару финансовых рынков (торговые символы) и визуализируем направления движения рынка на 1000 барах дневного таймфрейма. Если цена закрытия бара выше цены открытия, мы можем обозначить его как бычий бар (1), в противном случае — как медвежий (0).

import pandas as pd
import numpy as np

symbols = [
    "EURUSD",
    "USTEC",
    "XAUUSD",
    "USDJPY",
    "BTCUSD",
    "CA60",
    "UK100"
]

for symbol in symbols:
    
    df = pd.read_csv(fr"C:\Users\Omega Joctan\AppData\Roaming\MetaQuotes\Terminal\1640F6577B1C4EC659BF41EA9F6C38ED\MQL5\Files\{symbol}.PERIOD_D1.data.csv")
    df["Candle type"] = (df["Close"] > df["Open"]).astype(int)
    
    print(f"{symbol}(unique):",np.unique(df["Candle type"], return_counts=True))

Результат.

EURUSD(unique): (array([0, 1]), array([496, 504]))
USTEC(unique): (array([0, 1]), array([472, 528]))
XAUUSD(unique): (array([0, 1]), array([472, 528]))
USDJPY(unique): (array([0, 1]), array([408, 592]))
BTCUSD(unique): (array([0, 1]), array([478, 522]))
CA60(unique): (array([0, 1]), array([470, 530]))
UK100(unique): (array([0, 1]), array([463, 537]))

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

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

Допустим, мы хотим обучить модель для USDJPY на основе текущего набора данных, содержащего 1000 баров. У нас есть 408 медвежьих свечей (отмечены как 0), что составляет 40,8% от всех торговых сигналов, в то время как у нас есть 592 бычьи свечи (отмечены как 1), что составляет 59,2% от всех торговых сигналов.

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

Поскольку все модели стремятся достичь минимально возможного значения потерь при максимально возможной точности, они будут отдавать предпочтение бычьему тренду, который наблюдался в 59,2% случаев из 100%, как простому способу достижения максимальной точности.

Если вы игнорируете все прогнозы и все, что происходит на рынке, и используете только эти данные при прогнозировании USDJPY, вы можете сказать, что все бары будут бычьими постоянно, и вы будете правы примерно в 59,2% случаев. Неплохо, правда? Нет, неправда!

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

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


Недостатки несбалансированной целевой переменной в машинном обучении

  1. Низкие показатели для меньшего класса

    Как я уже говорил ранее, модель смещает прогнозы в сторону преобладающего класса, поскольку оптимизирует их для достижения общей точности. Например, в данных по выявлению мошенничества (99% — нет мошенничества, 1% — мошенничество), так как большинство людей не являются мошенниками, модель может всегда показывать отсутствие мошенничества и при этом достигать 99% точности, но при этом не обнаруживать случаи мошенничества.

  2. Вводящие в заблуждение показатели оценки

    Значение точности становится ненадежным: у модели может быть общая точность 72%, и она может не знать, что один класс был предсказан с точностью 95%, а другой — с точностью 50%.

  3. Модели переобучаются на представителях преобладающего класса

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

  4. Плохая обобщаемость по отношению неизвестным (реальным) данным

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


Методы решения проблемы несбалансированных наборов данных

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

01: Выбор подходящей метрики оценки

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

В задачах с несбалансированными данными другие метрики, такие как точность (precision), измеряющая, насколько точно классификатор предсказывает конкретный класс, и полнота (recall), измеряющая способность классификатора идентифицировать класс, гораздо полезнее, чем метрика точности (accuracy).

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

Это просто гармоническое среднее точности и полноты, представленное формулой.

F1-мера

Таким образом, если классификатор предсказывает принадлежность к меньшему классу, но предсказание ошибочно и количество ложноположительных результатов увеличивается, показатель точности будет низким, как и F1-мера.

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

F1-мера увеличивается только в том случае, если улучшается как количество, так и общее качество прогнозов.

Чтобы разобраться в этом подробнее, давайте обучим простой классификатор RandomForest на несбалансированном инструменте USDJPY.

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report

# Global variables

symbol = "USDJPY"
timeframe = "PERIOD_D1"
lookahead = 1
common_path = r"C:\Users\Omega Joctan\AppData\Roaming\MetaQuotes\Terminal\Common\Files"

df = pd.read_csv(f"{common_path}\{symbol}.{timeframe}.data.csv")

# Target variable

df["future_close"] = df["Close"].shift(-lookahead) # future closing price based on lookahead value
df.dropna(inplace=True)

df["Signal"] = (df["future_close"] > df["Close"]).astype(int)

print("Signals(unique): ",np.unique(df["Signal"], return_counts=True))

X = df.drop(columns=["Signal", "future_close"])
y = df["Signal"]

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

model = RandomForestClassifier(n_estimators=100,
                               max_depth=5,
                               min_samples_split=3,
                               random_state=42)

model.fit(X_train, y_train)

После обучения мы можем сохранить модель в формате ONNX для последующего использования в MetaTrader 5.

from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
import os

def saveModel(model, n_features: int, technique_name: str):
    
    initial_type = [("input", FloatTensorType([None, n_features]))]
    
    onnx_model = convert_sklearn(model, initial_types=initial_type, target_opset=14)

    with open(os.path.join(common_path, f"{symbol}.{timeframe}.{technique_name}.onnx"), "wb") as f:
        f.write(onnx_model.SerializeToString())
saveModel(model=model, n_features=X_train.shape[1], technique_name="no-sampling")

Мне пришлось использовать метод составления классификационных отчетов для анализа различных показателей в составе Scikit-Learn.

Train Classification report
               precision    recall  f1-score   support

           0       0.98      0.41      0.57       158
           1       0.68      1.00      0.81       204

    accuracy                           0.74       362
   macro avg       0.83      0.70      0.69       362
weighted avg       0.81      0.74      0.71       362

Анализ отчета о классификации обучения 

Общая точность обучения модели составляет 0,74, что на первый взгляд может показаться неплохим результатом. Однако более детальный анализ метрик по классам выявляет значительный дисбаланс в работе модели между двумя классами: класс 0 имеет очень высокую точность 0,98, но низкую полноту 0,41, что приводит к скромному показателю F1-меры 0,57.

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

В свою очередь, для класса 1 показатель полноты равен 1,00, а F1-мера — 0,81, но точность относительно ниже — 0,68.

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

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

Модель предсказывает класс 1 почти для всего и пропускает множество фактических образцов класса 0, что подтверждается низким значением показателя полноты для класса 0, равным 0,41.

В целом, эти показатели не только демонстрируют дисбаланс, но и вызывают опасения по поводу способности модели к обобщению и ее справедливости по отношению к классам. Здесь явно что-то не так.

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


Советник для тестирования

Всегда существует разница между результатами анализа модели машинного обучения, такими как отчет о классификации выше, и фактическими результатами торговли в MetaTrader 5. Поскольку мы будем сохранять модель в формате ONNX для последующего использования, мы можем создать простого торгового робота, который будет использовать модель, обученную на каждом из методов избыточной выборки (oversampling), обсуждаемых в этой статье, для принятия торговых решений на тестовой стратегии на обучающей выборке.

Использованные данные были собраны в файле Collectdata.mq5. Это скрипт, который собирает обучающие данные с 01.01.2025 по 01.01.2023. Его можно найти в приложениях к статье.

В советнике Test Resampling Techniques.mq5 мы инициализируем модель в формате ONNX, а затем используем ее для прогнозирования.

#include <Random Forest.mqh> 
CRandomForestClassifier random_forest; //A class for loading the RFC in ONNX format

#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>

CTrade m_trade;
CPositionInfo m_position;

input string symbol_ = "USDJPY";
input int magic_number= 14042025;
input int slippage = 100;
input ENUM_TIMEFRAMES timeframe_ = PERIOD_D1;
input string technique_name = "randomoversampling";

int lookahead = 1;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

   if (!random_forest.Init(StringFormat("%s.%s.%s.onnx", symbol_, EnumToString(timeframe_), technique_name), ONNX_COMMON_FOLDER)) //Initializing the RFC in ONNX format from a commmon folder
     return INIT_FAILED;
     
//--- Setting up the CTrade module
   
   m_trade.SetExpertMagicNumber(magic_number);
   m_trade.SetDeviationInPoints(slippage);
   m_trade.SetMarginMode();
   m_trade.SetTypeFillingBySymbol(symbol_);
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
      
   vector x = {
      iOpen(symbol_, timeframe_, 1),
      iHigh(symbol_, timeframe_, 1),
      iLow(symbol_, timeframe_, 1),
      iClose(symbol_, timeframe_, 1)
   };
   
   long signal = random_forest.predict_bin(x); //Predicted class
   double proba = random_forest.predict_proba(x).Max(); //Maximum predicted probability

   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) 
     {        
        if (!PosExists(POSITION_TYPE_BUY) && !PosExists(POSITION_TYPE_SELL))  
            m_trade.Buy(volume_, symbol_, ticks.ask,0,0);
     }
     
   if (signal == 0)
     {        
        if (!PosExists(POSITION_TYPE_SELL) && !PosExists(POSITION_TYPE_BUY))  
            m_trade.Sell(volume_, symbol_, ticks.bid,0,0);
     } 

//---
   
   CloseTradeAfterTime((Timeframe2Minutes(timeframe_)*lookahead)*60); //Close the trade after a certain lookahead and according the the trained timeframe
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool PosExists(ENUM_POSITION_TYPE type)
 {
    for (int i=PositionsTotal()-1; i>=0; i--)
      if (m_position.SelectByIndex(i))
         if (m_position.Symbol()==symbol_ && m_position.Magic() == magic_number && m_position.PositionType()==type)
            return (true);
            
    return (false);
 }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool ClosePos(ENUM_POSITION_TYPE type)
 {
    for (int i=PositionsTotal()-1; i>=0; i--)
      if (m_position.SelectByIndex(i))
         if (m_position.Symbol() == symbol_ && m_position.Magic() == magic_number && m_position.PositionType()==type)
            {
              if (m_trade.PositionClose(m_position.Ticket()))
                return true;
            }
            
    return (false);
 }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CloseTradeAfterTime(int period_seconds)
{
   for (int i = PositionsTotal() - 1; i >= 0; i--)
      if (m_position.SelectByIndex(i))
         if (m_position.Magic() == magic_number)
            if (TimeCurrent() - m_position.Time() >= period_seconds)
               m_trade.PositionClose(m_position.Ticket(), slippage);
}
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+

int Timeframe2Minutes(ENUM_TIMEFRAMES tf)
{
    switch(tf)
    {
        case PERIOD_M1:  return 1;
        case PERIOD_M2:  return 2;
        case PERIOD_M3:  return 3;
        case PERIOD_M4:  return 4;
        case PERIOD_M5:  return 5;
        case PERIOD_M6:  return 6;
        case PERIOD_M10: return 10;
        case PERIOD_M12: return 12;
        case PERIOD_M15: return 15;
        case PERIOD_M20: return 20;
        case PERIOD_M30: return 30;
        case PERIOD_H1:  return 60;
        case PERIOD_H2:  return 120;
        case PERIOD_H3:  return 180;
        case PERIOD_H4:  return 240;
        case PERIOD_H6:  return 360;
        case PERIOD_H8:  return 480;
        case PERIOD_H12: return 720;
        case PERIOD_D1:  return 1440; // 1 day = 1440 minutes
        case PERIOD_W1:  return 10080; // 1 week = 7 * 1440 minutes
        case PERIOD_MN1: return 43200; // Approx. 1 month = 30 * 1440 minutes

        default:
            PrintFormat("Unknown timeframe: %d", tf);
            return 0;
    }
}

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

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

Настройки тестера.

Входные параметры: technique_name = no-sampling.

Результаты тестирования.

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

Из 519 совершенных сделок 102 были короткими, что привело к 70,59% прибыльных сделок, в то время как из 519 совершенных сделок 417 были длинными, что привело к 60,19% точности выигрышных сделок. Здесь явно что-то не так, потому что если мы проанализируем направление свечей с 1 января 2023 года по 1 января 2025 года, исходя из значения прогнозируемого значения, равного 1:

print("classes in y: ",np.unique(y, return_counts=True))

Результат.

classes in y:  (array([0, 1]), array([225, 293]))

Мы видим, что 225 свечей были медвежьими, а 293 — бычьими. Поскольку большинство свечей двигались в бычьем направлении по USDJPY за этот двухлетний период (с 1 января 2023 года по 1 января 2025 года), любая, даже неудачная, модель, благоприятствующая бычьему движению, могла бы принести прибыль.

Теперь мы видим, что единственная причина, по которой модель принесла прибыль, заключалась в том, что она отдавала предпочтение длинным позициям в 4 раза чаще, чем коротким.

Поскольку в тот период рынок в основном демонстрировал бычий тренд, модель смогла получить некоторую прибыль.

Давайте перейдем к методам перевыборки (resampling) и посмотрим, как мы можем устранить это предвзятое принятие решений в наших моделях.


Методы избыточной выборки

Случайная избыточная выборка

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

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

Наиболее часто используемый инструмент для этой задачи - imbalanced-learn. Ниже приведен простой способ его использования.

from imblearn.over_sampling import RandomOverSampler

print("b4 Target: ",np.unique(y_train, return_counts=True))

rus = RandomOverSampler(random_state=42)
X_resampled, y_resampled = rus.fit_resample(X_train, y_train)

print("After Target: ",np.unique(y_resampled, return_counts=True))

Результаты.

b4 Target:  (array([0, 1]), array([304, 395]))
After Target:  (array([0, 1]), array([395, 395]))

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

model.fit(X_resampled, y_resampled)

Оценка модели.

y_train_pred = model.predict(X_train)

print("Train Classification report\n",classification_report(y_train, y_train_pred))

Результат.

Train Classification report
               precision    recall  f1-score   support

           0       0.82      0.85      0.83       158
           1       0.88      0.86      0.87       204

    accuracy                           0.85       362
   macro avg       0.85      0.85      0.85       362
weighted avg       0.85      0.85      0.85       362

Результаты указывают на значительное улучшение по всем показателям. Значения F1-меры, равные 0,87 для обоих классов, свидетельствуют о том, что модель делает непредвзятые и согласованные прогнозы, что указывает на хорошую обобщающую способность модели и равномерное распределение обучения по целевым классам.

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

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

Входные параметры: technique_name = randomoversampling.

Результаты тестирования.

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

В ходе этого тренировочного периода рынок продемонстрировал 225 медвежьих и 293 бычьих движения соответственно, в зависимости от того, как мы сформулировали целевую переменную. Новая модель, обученная на данных с избыточной выборкой, открыла 238 коротких и 281 длинную позицию соответственно. Это хороший знак - скорее всего, модель не является предвзятой, поскольку принимает решения, основываясь на изученных закономерностях, а не на чем-либо другом.


Методы неполной выборки

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

Случайная неполная выборка

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

Этот метод предполагает случайное или стратегическое удаление образцов из преобладающего класса.

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

from imblearn.under_sampling import RandomUnderSampler

print("b4 Target: ",np.unique(y_train, return_counts=True))

rus = RandomUnderSampler(random_state=42)
X_resampled, y_resampled = rus.fit_resample(X_train, y_train)

print("After Target: ",np.unique(y_resampled, return_counts=True))

Результат.

b4 Target:  (array([0, 1]), array([304, 395]))
After Target:  (array([0, 1]), array([304, 304]))

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

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

Train Classification report
               precision    recall  f1-score   support

           0       0.76      0.90      0.82       158
           1       0.91      0.78      0.84       204

    accuracy                           0.83       362
   macro avg       0.83      0.84      0.83       362
weighted avg       0.84      0.83      0.83       362

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

Входные параметры: technique_name = randomundersampling.

Результаты тестирования.

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

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

Связи Томека

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

Представьте, что у нас есть две точки A и B, принадлежащие к разным классам: A принадлежит к преобладающему классу, а B — к классу меньшинства (или наоборот).

Если эти две точки (A и B) находятся близко друг к другу (являются соседями), то наблюдение из преобладающего класса (в данном случае A) будет удалено.

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

from imblearn.under_sampling import TomekLinks

tl = TomekLinks()
X_resampled, y_resampled = tl.fit_resample(X_train, y_train)

print(f"Before --> y (unique): {np.unique(y_train, return_counts=True)}\nAfter  --> y (unique): {np.unique(y_resampled, return_counts=True)}")

Результаты.

Before --> y (unique): (array([0, 1]), array([304, 395]))
After  --> y (unique): (array([0, 1]), array([304, 283]))

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

Этот метод также показал лучшие результаты на обучающих данных.

Train Classification report
               precision    recall  f1-score   support

           0       0.69      0.94      0.80       158
           1       0.93      0.68      0.78       204

    accuracy                           0.79       362
   macro avg       0.81      0.81      0.79       362
weighted avg       0.83      0.79      0.79       362

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

Входные параметры: technique_name = tomek-links.

Результаты тестирования.

Аналогично методу случайной неполной выборки, метод связей Томека отдавал предпочтение коротким позициям, открыв 303 короткие позиции и 216 длинных, и все же смог получить прибыль.

Центроиды кластеров

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

Он работает следующим образом.

  1. Для кластеризации преобладающего класса применяется алгоритм k-средних.
  2. Выбирается K-число необходимых образцов.
  3. Большинство образцов в классе заменены центрами кластеров k.
  4. Полученные данные объединяются с меньшим классом для создания сбалансированного набора данных.

from imblearn.under_sampling import ClusterCentroids

cc = ClusterCentroids(random_state=42)
X_resampled, y_resampled = cc.fit_resample(X, y)

print(f"Before --> y (unique): {np.unique(y_train, return_counts=True)}\nAfter  --> y (unique): {np.unique(y_resampled, return_counts=True)}")

Результаты.

Before --> y (unique): (array([0, 1]), array([158, 204]))
After  --> y (unique): (array([0, 1]), array([225, 225]))

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

Train Classification report
               precision    recall  f1-score   support

           0       0.64      0.86      0.73       158
           1       0.85      0.62      0.72       204

    accuracy                           0.73       362
   macro avg       0.75      0.74      0.73       362
weighted avg       0.76      0.73      0.73       362

На данный момент этот метод демонстрирует наименьшую точность: значение 0,73 может указывать на меньшую степень переобучения модели по сравнению с предыдущими, поэтому, возможно, это лучшая из возможных моделей на данный момент. Давайте оценим ее точность в реальной торговле.

Входные параметры: technique_name = cluster-centroids.

Результаты тестирования.

Этот метод обеспечил наибольшее количество прибыльных сделок — 343 сделки из 519, с точностью 66,09%, поскольку прибыль почти достигла первоначального депозита. Несмотря на то, что модель отдавала предпочтение коротким позициям, она очень точно предсказывала бычьи сигналы, что привело к впечатляющим 75,97% из 100% выигрышных длинных позиций.


Гибридные методы

SMOTE + связи Томека

Сначала применяется SMOTE, затем с помощью связей Томека удаляется шум.

from imblearn.combine import SMOTETomek

smt = SMOTETomek(random_state=42)
X_resampled, y_resampled = smt.fit_resample(X_train, y_train)

print(f"Before --> y (unique): {np.unique(y_train, return_counts=True)}\nAfter  --> y (unique): {np.unique(y_resampled, return_counts=True)}")

Результаты.

Before --> y (unique): (array([0, 1]), array([158, 204]))
After  --> y (unique): (array([0, 1]), array([159, 159]))

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

Train Classification report
               precision    recall  f1-score   support

           0       0.74      0.73      0.73       158
           1       0.79      0.80      0.80       204

    accuracy                           0.77       362
   macro avg       0.77      0.77      0.77       362
weighted avg       0.77      0.77      0.77       362

Ниже представлены торговые результаты.

Входные параметры: technique_name = smote-tomeklinks.

Результаты тестирования.

220 коротких и 299 длинных сделок соответственно. Неплохо.

SMOTE + ENN (отредактированные ближайшие соседи)

SMOTE генерирует синтетические образцы, а затем ENN удаляет неправильно классифицированные образцы.

from imblearn.combine import SMOTEENN

sme = SMOTEENN(random_state=42)
X_resampled, y_resampled = sme.fit_resample(X_train, y_train)

print(f"Before --> y (unique): {np.unique(y_train, return_counts=True)}\nAfter  --> y (unique): {np.unique(y_resampled, return_counts=True)}")

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

Before --> y (unique): (array([0, 1]), array([158, 204]))
After  --> y (unique): (array([0, 1]), array([37, 24]))

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

Train Classification report
               precision    recall  f1-score   support

           0       0.46      0.76      0.58       158
           1       0.63      0.32      0.42       204

    accuracy                           0.51       362
   macro avg       0.55      0.54      0.50       362
weighted avg       0.56      0.51      0.49       362

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

Входные параметры: technique_name = smote-enn.

Результаты тестирования.

Этот метод нисколько не помог, а только усугубил ситуацию. Он исказил результаты торговли, поскольку робот открыл 180 из 519 сделок на покупку и 339 сделок на продажу.

Это не значит, что метод плохой, просто в данной ситуации он не оптимален.


Заключение

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

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

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

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

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


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

Имя файла Описание/использование
Experts\Test Resampling Techniques.mq5
Советник для развертывания .ONNX-файлов в MQL5
Include\pandas.mqh
Библиотека Pandas для обработки и хранения данных
Scripts\Collectdata.mq5
Скрипт для сбора обучающих данных
Common\*.onnx
Модели машинного обучения в формате ONNX
Common\*.csv
Использование обучающих данных различных инструментов в машинном обучении


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

Прикрепленные файлы |
Attachments.zip (153.84 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
linfo2
linfo2 | 23 апр. 2025 в 22:56
Спасибо, вы, Омега, цените, что вы собрали это вместе, Баис - это то, чего мы все боимся. Я скачал вложения, могу ли я предложить, что он включает в себя все необходимые компоненты. К счастью, у вас есть github, поэтому я смог найти и установить необходимые компоненты (preprossing.mqh, plots.mqh, Matrixextend.mqh, metrics.mqh и Random Forext.mqh). К сожалению, затем я застрял с сообщением ' Init - Undeclared Identifier ' из строки if (!random_forest.Init(StringFormat("%s.%s.%s.onnx", symbol_, EnumToString(timeframe_), technique_name), ONNX_COMMON_FOLDER)) //Инициализация RFC в формате ONNX из общей папки. Я проверил, и у меня действительно есть USDJPY.PERIOD_D1.randomundersampling.onnx в папке MQL5\Common
Omega J Msigwa
Omega J Msigwa | 24 апр. 2025 в 06:59
linfo2 USDJPY.PERIOD_D1.randomundersampling.onnx в папке MQL5\Common

Необходимые компоненты - это последние версии всего, что импортировано в блокнот, вы можете выполнить pip install, не беспокоясь о конфликте версий. Также вы можете перейти по ссылке в таблице вложений, она приведет вас на сайт Kaggle.com, где вы сможете редактировать и изменять код.

Необъявленный идентификатор может означать, что переменная или объект не определены. Проверьте свой код или пришлите мне скриншот кода по DM.

zhai nan
zhai nan | 14 июн. 2025 в 11:52
Результаты тестирования недостоверны. Временной период тестирования включает в себя временной период обучающих данных. Модель будет хорошо работать на обучающем наборе и плохо - на тестовом. Вы можете видеть, что многие результаты тестирования находятся во временном диапазоне от 2024.07.06 до 2025.01.01. Модель работает плохо, в то время как в предыдущее время она работает отлично. Я подсчитал, что обучающий набор составляет 80 %. Данные не подвергаются случайному распределению, а обучающий и тестовый наборы распределяются по порядку. Модель распознает предыдущие обучающие данные, но не последующие тестовые данные. Вы использовали обучающие данные в тесте, что привело к завышению общих результатов, а производительность на последующих тестовых данных очень низкая, с потерями. Тогда центроид кластера также ненадежен. При кластеризации используются все данные, и общие данные будут использоваться для кластеризации и последующего обучения. Таким образом, обучающие данные, используемые этим методом, включают временной диапазон 2023.01.01~2025.01.01. Можно сказать, что это тест полностью на обучающих данных, без тестирования на неизвестных данных. Тренировочные и тестовые данные должны быть строго разграничены в хронологическом порядке. Для проверки эффективности модели можно использовать только тестовые данные, например, 2023.01.01~2024.01.01 в качестве обучающих данных, а 2024.01.01~2025.01.01 в качестве тестовых данных. Тестовая модель может быть проверена только на тестовых данных. Не должно быть риска утечки информации, иначе все хорошие показатели модели окажутся недостоверными.
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Алгоритм оптимизации бабочек — Butterfly Optimization Algorithm (BOA) Алгоритм оптимизации бабочек — Butterfly Optimization Algorithm (BOA)
В статье рассмотрен алгоритм оптимизации бабочек, основанный на моделировании поиска пищи с помощью обоняния. Проведён анализ оригинальных формул, выявлена и исправлена ошибка в уравнениях движения, добавлен механизм поддержания разнообразия популяции, представлены результаты тестирования.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Знакомство с языком MQL5 (Часть 29): Освоение API и функции WebRequest в языке MQL5 (III) Знакомство с языком MQL5 (Часть 29): Освоение API и функции WebRequest в языке MQL5 (III)
В этой статье мы продолжаем осваивать API и WebRequest в языке MQL5, получая свечные данные из внешнего источника. Мы разберем ответ сервера, очистим данные и извлечем ключевые элементы – время открытия и значения OHLC для нескольких дневных свечей, подготовив все для дальнейшего анализа.