
Изучение передовых методов машинного обучения в стратегии пробоя «коридора Дарваса» (Darvas Box Breakout)
Введение
Стратегия Darvas Box Breakout, созданная Николасом Дарвасом, представляет собой подход в технической торговле, который выявляет потенциальные сигналы на покупку, когда цена акций поднимается выше установленного диапазона «коридора», что указывает на сильный восходящий импульс. В этой статье мы применим эту стратегическую концепцию в качестве примера для изучения трех передовых методов машинного обучения. К ним относятся использование модели машинного обучения для генерации сигналов вместо фильтрации сделок, применение непрерывных сигналов вместо дискретных и использование для подтверждения сделок моделей, обученных на разных таймфреймах. Эти методы открывают новые перспективы возможностей машинного обучения в области улучшения алгоритмической торговли за пределами традиционных способов.
В этой статье подробно рассматриваются свойства и теоретические основы трех передовых методов, редко освещаемых специалистами по обучению, поскольку они являются инновационными по сравнению с традиционными методами. Кроме того, будут рассмотрены такие сложные темы, как проектирование характеристик и настройка гиперпараметров в процессе обучения модели. Однако статья не будет рассматривать во всех подробностях каждый этап процесса формирования модели машинного обучения. Читателям, которые интересуются пропущенными процедурами, предлагаю проверить эту ссылку на статью, где описан весь процесс реализации.
Генерация сигналов
Машинное обучение может быть трех основных типов: контролируемое обучение, неконтролируемое обучение и обучение с подкреплением. В алгоритмизированной торговле трейдеры чаще всего предпочитают другим типам контролируемое обучение по двум основным причинам. - Неконтролируемое обучение часто оказывается слишком примитивным, чтобы уловить сложные взаимосвязи между результатами торговли и характеристиками рынка. Без меток оно с трудом справляется с выполнением целей прогнозирования и больше подходит для предсказания косвенных данных, а не прямых результатов торговой стратегии.
- Обучение с подкреплением требует создания обучающей среды с функцией вознаграждения, направленной на доведения прибыли до максимума в долгосрочной перспективе, а не сосредоточенной на точных индивидуальных прогнозах. Такой подход подразумевает сложную настройку для простой задачи прогнозирования результатов, что делает его менее рентабельным для розничных трейдеров.
Тем не менее, контролируемое обучение предлагает множество возможностей применения в алгоритмической торговле. Распространенный метод — использование его в качестве фильтра: вы начинаете с оригинальной стратегии, которая генерирует множество образцов, а затем обучаете модель, чтобы определить, когда, вероятнее всего, стратегия будет успешной или потерпит неудачу. Уровень доверительной вероятности модели помогает отфильтровывать неудачные сделки, которые она предсказывает.
Другой подход, который мы рассмотрим в этой статье, заключается в использовании контролируемого обучения для генерации сигналов. Для типичных задач регрессии, таких как прогнозирование цены, это просто: покупайте, если модель прогнозирует рост цены, и продавайте, когда она предсказывает падение. Но как совместить это с такой базовой стратегией, как Darvas Box Breakout?
Сначала разработаем советник для сбора необходимых данных о свойствах и метках для последующего обучения модели на языке Python.
Стратегия Darvas Box Breakout задает «коридор» с помощью серии свечей отклонения после максимума или минимума, инициируя сделку при пробое ценой границ этого диапазона. В любом случае нам нужен сигнал, чтобы начать собирать данные о характеристиках и прогнозировать будущие результаты. Итак, мы установим срабатывание на момент пробоя ценой нижнего или верхнего предела диапазона. Эта функция определяет, существует ли «коридор Дарваса» для заданного периода ретроспективного анализа и количества подтверждающих свечей, присваивает переменным значение максимального (минимального) диапазона и отображает «коробку» на графике.
double high; double low; bool boxFormed = false; bool DetectDarvasBox(int n = 100, int M = 3) { // Clear previous Darvas box objects for (int k = ObjectsTotal(0, 0, -1) - 1; k >= 0; k--) { string name = ObjectName(0, k); if (StringFind(name, "DarvasBox_") == 0) ObjectDelete(0, name); } bool current_box_active = false; // Start checking from the oldest bar within the lookback period for (int i = M+1; i <= n; i++) { // Get high of current bar and previous bar double high_current = iHigh(_Symbol, PERIOD_CURRENT, i); double high_prev = iHigh(_Symbol, PERIOD_CURRENT, i + 1); // Check for a new high if (high_current > high_prev) { // Check if the next M bars do not exceed the high bool pullback = true; for (int k = 1; k <= M; k++) { if (i - k < 0) // Ensure we don't go beyond available bars { pullback = false; break; } double high_next = iHigh(_Symbol, PERIOD_CURRENT, i - k); if (high_next > high_current) { pullback = false; break; } } // If pullback condition is met, define the box if (pullback) { double top = high_current; double bottom = iLow(_Symbol, PERIOD_CURRENT, i); // Find the lowest low over the bar and the next M bars for (int k = 1; k <= M; k++) { double low_next = iLow(_Symbol, PERIOD_CURRENT, i - k); if (low_next < bottom) bottom = low_next; } // Check for breakout from i - M - 1 to the current bar (index 0) int j = i - M - 1; while (j >= 0) { double close_j = iClose(_Symbol, PERIOD_CURRENT, j); if (close_j > top || close_j < bottom) break; // Breakout found j--; } j++; // Adjust to the bar after breakout (or 0 if no breakout) // Create a unique object name string obj_name = "DarvasBox_" + IntegerToString(i); // Plot the box datetime time_start = iTime(_Symbol, PERIOD_CURRENT, i); datetime time_end; if (j > 0) { // Historical box: ends at breakout time_end = iTime(_Symbol, PERIOD_CURRENT, j); } else { // Current box: extends to the current bar time_end = iTime(_Symbol, PERIOD_CURRENT, 0); current_box_active = true; } high = top; low = bottom; ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0, time_start, top, time_end, bottom); ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue); ObjectSetInteger(0, obj_name, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 1); boxFormed = true; // Since we're only plotting the most recent box, break after finding it break; } } } return current_box_active; }
Вот несколько примеров «коридора Дарваса» на графике:
По сравнению с использованием его в качестве фильтра этот метод имеет недостатки. Нам потребуется спрогнозировать сбалансированные результаты с равными вероятностями, например, будут ли следующие 10 баров выше или ниже, или: переместится цена на 10 пунктов сначала вверх или вниз. Другим недостатком является потеря нами зафиксированного концевого элемента базовой стратегии — преимущество полностью зависит от прогностических возможностей модели. Положительный момент — вы не ограничиваетесь выборками, которые базовая стратегия предоставляет только при своем срабатывании, что дает больший начальный размер выборки и больший потенциал роста. Реализуем торговую логику в функции onTick() следующим образом:
input int checkBar = 30; input int lookBack = 100; input int countMax = 10; void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars; boxFormed = false; bool NotInPosition = true; lastClose = iClose(_Symbol, PERIOD_CURRENT, 1); lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2); for(int i = 0; i<PositionsTotal(); i++){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;} /*count++; if(count >=countMax ){ trade.PositionClose(pos); count = 0;} }}*/ DetectDarvasBox(lookBack,checkBar); if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low)))executeBuy(); } }
Для этой стратегии использование того же размера тейк-профита и стоп-лосса является более последовательным, чем отслеживание результатов следующих 10 баров. Первый вариант непосредственно связывает наш прогноз с окончательной прибылью, а второй добавляет неопределенности, поскольку доходность варьируется на каждом 10-барном периоде. Стоит отметить, что мы используем тейк-профит и стоп-лосс в процентах от цены, делая этот подход более гибким в адаптации к различным активам, и он лучше подходит для трендовых активов, таких как золото или индексы. Читатели могут протестировать альтернативный вариант, раскомментировав закомментированный код и удалив тейк-профит и стоп-лосс из функции покупки.
В качестве сведений о характеристиках, используемых для прогнозирования результатов, я выбрал три последних нормализованных доходности, нормализованное расстояние от максимального и минимального значений диапазона, а также некоторые распространенные стационарные индикаторы. Сохраняем эти данные в многомерном массиве, который затем сохраняется в файле CSV с использованием класса CFileCSV из включенного файла. Убедимся, что все таймфреймы и символы установлены так, как указано ниже, чтобы можно было легко переключаться между таймфреймами и активами.string data[50000][12]; int indexx = 0; void getData(){ double close = iClose(_Symbol,PERIOD_CURRENT,1); double close2 = iClose(_Symbol,PERIOD_CURRENT,2); double close3 = iClose(_Symbol,PERIOD_CURRENT,3); double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close; double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2; double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3; double highDistance = 1000*(close-high)/close; double lowDistance = 1000*(close-low)/close; double boxSize = 1000*(high-low)/close; double adx[]; // Average Directional Movement Index double wilder[]; // Average Directional Movement Index by Welles Wilder double dm[]; // DeMarker double rsi[]; // Relative Strength Index double rvi[]; // Relative Vigor Index double sto[]; // Stochastic Oscillator CopyBuffer(handleAdx, 0, 1, 1, adx); // Average Directional Movement Index CopyBuffer(handleWilder, 0, 1, 1, wilder); // Average Directional Movement Index by Welles Wilder CopyBuffer(handleDm, 0, 1, 1, dm); // DeMarker CopyBuffer(handleRsi, 0, 1, 1, rsi); // Relative Strength Index CopyBuffer(handleRvi, 0, 1, 1, rvi); // Relative Vigor Index CopyBuffer(handleSto, 0, 1, 1, sto); // Stochastic Oscillator //2 means 2 decimal places data[indexx][0] = DoubleToString(adx[0], 2); // Average Directional Movement Index data[indexx][1] = DoubleToString(wilder[0], 2); // Average Directional Movement Index by Welles Wilder data[indexx][2] = DoubleToString(dm[0], 2); // DeMarker data[indexx][3] = DoubleToString(rsi[0], 2); // Relative Strength Index data[indexx][4] = DoubleToString(rvi[0], 2); // Relative Vigor Index data[indexx][5] = DoubleToString(sto[0], 2); // Stochastic Oscillator data[indexx][6] = DoubleToString(stationary,2); data[indexx][7] = DoubleToString(boxSize,2); data[indexx][8] = DoubleToString(stationary2,2); data[indexx][9] = DoubleToString(stationary3,2); data[indexx][10] = DoubleToString(highDistance,2); data[indexx][11] = DoubleToString(lowDistance,2); indexx++; }
Окончательный код советника для выборки данных будет выглядеть следующим образом:
#include <Trade/Trade.mqh> CTrade trade; #include <FileCSV.mqh> CFileCSV csvFile; string fileName = "box.csv"; string headers[] = { "Average Directional Movement Index", "Average Directional Movement Index by Welles Wilder", "DeMarker", "Relative Strength Index", "Relative Vigor Index", "Stochastic Oscillator", "Stationary", "Box Size", "Stationary2", "Stationary3", "Distance High", "Distance Low" }; input double lott = 0.01; input int Magic = 0; input int checkBar = 30; input int lookBack = 100; input int countMax = 10; input double slp = 0.003; input double tpp = 0.003; input bool saveData = true; string data[50000][12]; int indexx = 0; int barsTotal = 0; int count = 0; double high; double low; bool boxFormed = false; double lastClose; double lastlastClose; int handleAdx; // Average Directional Movement Index - 3 int handleWilder; // Average Directional Movement Index by Welles Wilder - 3 int handleDm; // DeMarker - 1 int handleRsi; // Relative Strength Index - 1 int handleRvi; // Relative Vigor Index - 2 int handleSto; // Stochastic Oscillator - 2 int OnInit() { trade.SetExpertMagicNumber(Magic); handleAdx=iADX(_Symbol,PERIOD_CURRENT,14);//Average Directional Movement Index - 3 handleWilder=iADXWilder(_Symbol,PERIOD_CURRENT,14);//Average Directional Movement Index by Welles Wilder - 3 handleDm=iDeMarker(_Symbol,PERIOD_CURRENT,14);//DeMarker - 1 handleRsi=iRSI(_Symbol,PERIOD_CURRENT,14,PRICE_CLOSE);//Relative Strength Index - 1 handleRvi=iRVI(_Symbol,PERIOD_CURRENT,10);//Relative Vigor Index - 2 handleSto=iStochastic(_Symbol,PERIOD_CURRENT,5,3,3,MODE_SMA,STO_LOWHIGH);//Stochastic Oscillator - 2 return(INIT_SUCCEEDED); } void OnDeinit(const int reason) { if (!saveData) return; if(csvFile.Open(fileName, FILE_WRITE|FILE_ANSI)) { //Write the header csvFile.WriteHeader(headers); //Write data rows csvFile.WriteLine(data); //Close the file csvFile.Close(); } else { Print("File opening error!"); } } void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars; boxFormed = false; bool NotInPosition = true; lastClose = iClose(_Symbol, PERIOD_CURRENT, 1); lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2); for(int i = 0; i<PositionsTotal(); i++){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;} /*count++; if(count >=countMax ){ trade.PositionClose(pos); count = 0;} }}*/ DetectDarvasBox(lookBack,checkBar); if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low)))executeBuy(); } } void executeBuy() { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); double sl = lastClose*(1-slp); double tp = lastClose*(1+tpp); trade.Buy(lott,_Symbol,ask,sl,tp); if(PositionsTotal()>0)getData(); } bool DetectDarvasBox(int n = 100, int M = 3) { // Clear previous Darvas box objects for (int k = ObjectsTotal(0, 0, -1) - 1; k >= 0; k--) { string name = ObjectName(0, k); if (StringFind(name, "DarvasBox_") == 0) ObjectDelete(0, name); } bool current_box_active = false; // Start checking from the oldest bar within the lookback period for (int i = M+1; i <= n; i++) { // Get high of current bar and previous bar double high_current = iHigh(_Symbol, PERIOD_CURRENT, i); double high_prev = iHigh(_Symbol, PERIOD_CURRENT, i + 1); // Check for a new high if (high_current > high_prev) { // Check if the next M bars do not exceed the high bool pullback = true; for (int k = 1; k <= M; k++) { if (i - k < 0) // Ensure we don't go beyond available bars { pullback = false; break; } double high_next = iHigh(_Symbol, PERIOD_CURRENT, i - k); if (high_next > high_current) { pullback = false; break; } } // If pullback condition is met, define the box if (pullback) { double top = high_current; double bottom = iLow(_Symbol, PERIOD_CURRENT, i); // Find the lowest low over the bar and the next M bars for (int k = 1; k <= M; k++) { double low_next = iLow(_Symbol, PERIOD_CURRENT, i - k); if (low_next < bottom) bottom = low_next; } // Check for breakout from i - M - 1 to the current bar (index 0) int j = i - M - 1; while (j >= 0) { double close_j = iClose(_Symbol, PERIOD_CURRENT, j); if (close_j > top || close_j < bottom) break; // Breakout found j--; } j++; // Adjust to the bar after breakout (or 0 if no breakout) // Create a unique object name string obj_name = "DarvasBox_" + IntegerToString(i); // Plot the box datetime time_start = iTime(_Symbol, PERIOD_CURRENT, i); datetime time_end; if (j > 0) { // Historical box: ends at breakout time_end = iTime(_Symbol, PERIOD_CURRENT, j); } else { // Current box: extends to the current bar time_end = iTime(_Symbol, PERIOD_CURRENT, 0); current_box_active = true; } high = top; low = bottom; ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0, time_start, top, time_end, bottom); ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue); ObjectSetInteger(0, obj_name, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 1); boxFormed = true; // Since we're only plotting the most recent box, break after finding it break; } } } return current_box_active; } void getData(){ double close = iClose(_Symbol,PERIOD_CURRENT,1); double close2 = iClose(_Symbol,PERIOD_CURRENT,2); double close3 = iClose(_Symbol,PERIOD_CURRENT,3); double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close; double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2; double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3; double highDistance = 1000*(close-high)/close; double lowDistance = 1000*(close-low)/close; double boxSize = 1000*(high-low)/close; double adx[]; // Average Directional Movement Index double wilder[]; // Average Directional Movement Index by Welles Wilder double dm[]; // DeMarker double rsi[]; // Relative Strength Index double rvi[]; // Relative Vigor Index double sto[]; // Stochastic Oscillator CopyBuffer(handleAdx, 0, 1, 1, adx); // Average Directional Movement Index CopyBuffer(handleWilder, 0, 1, 1, wilder); // Average Directional Movement Index by Welles Wilder CopyBuffer(handleDm, 0, 1, 1, dm); // DeMarker CopyBuffer(handleRsi, 0, 1, 1, rsi); // Relative Strength Index CopyBuffer(handleRvi, 0, 1, 1, rvi); // Relative Vigor Index CopyBuffer(handleSto, 0, 1, 1, sto); // Stochastic Oscillator //2 means 2 decimal places data[indexx][0] = DoubleToString(adx[0], 2); // Average Directional Movement Index data[indexx][1] = DoubleToString(wilder[0], 2); // Average Directional Movement Index by Welles Wilder data[indexx][2] = DoubleToString(dm[0], 2); // DeMarker data[indexx][3] = DoubleToString(rsi[0], 2); // Relative Strength Index data[indexx][4] = DoubleToString(rvi[0], 2); // Relative Vigor Index data[indexx][5] = DoubleToString(sto[0], 2); // Stochastic Oscillator data[indexx][6] = DoubleToString(stationary,2); data[indexx][7] = DoubleToString(boxSize,2); data[indexx][8] = DoubleToString(stationary2,2); data[indexx][9] = DoubleToString(stationary3,2); data[indexx][10] = DoubleToString(highDistance,2); data[indexx][11] = DoubleToString(lowDistance,2); indexx++; }
Мы намерены торговать по этой стратегии по символу XAUUSD на 15-минутном таймфрейме из-за высокой волатильности этого актива, а также потому, что 15 минут находит золотую середину между снижением шума и генерацией большего количества выборок. Типичная сделка будет выглядеть примерно так:
В качестве обучающих и проверочных данных используем данные за 2020-2024 годы, а позднее протестируем результат на данных за 2024-2025 годы в терминале MetaTrader 5. После запуска этого советника в тестере стратегий CSV-файл будет сохранен в каталоге /Tester/Agent-sth000 после деинициализации советника.
Кроме того, щелкнем правой клавишей мыши, чтобы получить отчет о тестировании на исторических данных в Excel, как показано ниже:
Обратите внимание на номер строки «Сделки», который мы позднее будем использовать в качестве входных данных.
После этого обучим модель на языке Python.
Модель, которую мы выбрали для этой статьи, представляет собой вариант на основе дерева решений, идеально подходящий для задач классификации, как и та, которую мы использовали в этой статье.
import pandas as pd # Replace 'your_file.xlsx' with the path to your file input_file = 'box.xlsx' # Load the Excel file and skip the first {skiprows} rows data1 = pd.read_excel(input_file, skiprows=4417) # Select the 'profit' column (assumed to be 'Unnamed: 10') and filter rows as per your instructions profit_data = data1["Profit"][1:-1] profit_data = profit_data[profit_data.index % 2 == 0] # Filter for rows with odd indices profit_data = profit_data.reset_index(drop=True) # Reset index # Convert to float, then apply the condition to set values to 1 if > 0, otherwise to 0 profit_data = pd.to_numeric(profit_data, errors='coerce').fillna(0) # Convert to float, replacing NaN with 0 profit_data = profit_data.apply(lambda x: 1 if x > 0 else 0) # Apply condition # Load the CSV file with semicolon separator file_path = 'box.csv' data2 = pd.read_csv(file_path, sep=';') # Drop rows with any missing or incomplete values data2.dropna(inplace=True) # Drop any duplicate rows if present data2.drop_duplicates(inplace=True) # Convert non-numeric columns to numerical format for col in data2.columns: if data2[col].dtype == 'object': # Convert categorical to numerical using label encoding data2[col] = data2[col].astype('category').cat.codes # Ensure all remaining columns are numeric and cleanly formatted for CatBoost data2 = data2.apply(pd.to_numeric, errors='coerce') data2.dropna(inplace=True) # Drop any rows that might still contain NaNs after conversion # Merge the two DataFrames on the index merged_data = pd.merge(profit_data, data2, left_index=True, right_index=True, how='inner') # Save the merged data to a new CSV file output_csv_path = 'merged_data.csv' merged_data.to_csv(output_csv_path) print(f"Merged data saved to {output_csv_path}")Используем этот код для маркировки отчета в Excel, присваивая 1 сделкам с положительной прибылью и 0 — сделкам без нее. Затем объединим его с данными о свойствах, собранными из CSV-файла советника, извлекающего данные. Имейте в виду, что значение skiprow соответствует номеру строки в «Сделках».
import numpy as np import pandas as pd import warnings warnings.filterwarnings("ignore") data = pd.read_csv("merged_data.csv",index_col=0) XX = data.drop(columns=['Profit']) yy = data['Profit'] y = yy.values X = XX.values pd.DataFrame(X,y)
Затем присваиваем массив меток переменной y, а фрейм данных свойств — переменной X.
import numpy as np import pandas as pd import warnings import seaborn as sns warnings.filterwarnings("ignore") from sklearn.model_selection import train_test_split import catboost as cb from sklearn.metrics import roc_curve, roc_auc_score import matplotlib.pyplot as plt # Split data X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, shuffle=False) # Identify categorical features cat_feature_indices = [i for i, col in enumerate(XX.columns) if XX[col].dtype == 'object'] # Train CatBoost classifier model = cb.CatBoostClassifier( iterations=5000, # Number of trees (similar to n_estimators) learning_rate=0.02, # Learning rate depth=5, # Depth of each tree l2_leaf_reg=5, bagging_temperature=1, early_stopping_rounds=50, loss_function='Logloss', # Use 'MultiClass' if it's a multi-class problem verbose=1000) model.fit(X_train, y_train, cat_features=cat_feature_indices)
Затем разделим данные в соотношении 9:1 на обучающий и проверочный наборы и начнем обучение модели. Настройка по умолчанию для функции разделения обучающих и тестовых данных в sklearn включает в себя перемешивание, что не идеально для данных временных рядов, поэтому обязательно задайте параметр shuffle=False в параметрах. Хорошая идея — настроить гиперпараметры, чтобы избежать переобучения или недообучения в зависимости от размера выборки. Лично я обнаружил, что прекращение итераций при значении потери логарифма около 0.1 работает хорошо.
import numpy as np from sklearn.metrics import roc_curve, roc_auc_score import matplotlib.pyplot as plt # Assuming you already have y_test, X_test, and model defined # Predict probabilities y_prob = model.predict_proba(X_test)[:, 1] # Probability for positive class # Compute ROC curve and AUC (for reference) fpr, tpr, thresholds = roc_curve(y_test, y_prob) auc_score = roc_auc_score(y_test, y_prob) print(f"AUC Score: {auc_score:.2f}") # Define confidence thresholds to test (e.g., 50%, 60%, 70%, etc.) confidence_thresholds = np.arange(0.5, 1.0, 0.05) # From 50% to 95% in steps of 5% accuracies = [] coverage = [] # Fraction of samples classified at each threshold for thresh in confidence_thresholds: # Classify only when probability is >= thresh (positive) or <= (1 - thresh) (negative) y_pred_confident = np.where(y_prob >= thresh, 1, np.where(y_prob <= (1 - thresh), 0, -1)) # Filter out unclassified samples (where y_pred_confident == -1) mask = y_pred_confident != -1 y_test_confident = y_test[mask] y_pred_confident = y_pred_confident[mask] # Calculate accuracy and coverage if len(y_test_confident) > 0: # Avoid division by zero acc = np.mean(y_pred_confident == y_test_confident) cov = len(y_test_confident) / len(y_test) else: acc = 0 cov = 0 accuracies.append(acc) coverage.append(cov) # Plot Accuracy vs Confidence Threshold plt.figure(figsize=(10, 6)) plt.plot(confidence_thresholds, accuracies, marker='o', label='Accuracy', color='blue') plt.plot(confidence_thresholds, coverage, marker='s', label='Coverage', color='green') plt.xlabel('Confidence Threshold') plt.ylabel('Metric Value') plt.title('Accuracy and Coverage vs Confidence Threshold') plt.legend(loc='best') plt.grid(True) plt.show() # Also show the original ROC curve for reference plt.figure(figsize=(8, 6)) plt.plot(fpr, tpr, label=f'ROC Curve (AUC = {auc_score:.2f})', color='blue') plt.plot([0, 1], [0, 1], 'k--', label='Random Guess') plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.title('Receiver Operating Characteristic (ROC) Curve') plt.legend(loc='lower right') plt.grid(True) plt.show()
Затем построим визуализации результатов для проверки валидационного теста. При стандартном подходе «обучение-проверка-тестирование» этап проверки помогает выбрать лучшие гиперпараметры и изначально оценивает, обладает ли обученная модель прогностическими возможностями. Он действует как буфер перед переходом к финальному тесту.
# Feature importance feature_importance = model.get_feature_importance() importance_df = pd.DataFrame({ 'feature': XX.columns, 'importance': feature_importance }).sort_values('importance', ascending=False) print("Feature Importances:") print(importance_df) plt.figure(figsize=(10, 6)) sns.barplot(x='importance', y='feature', data=importance_df) plt.title(' Feature Importance') plt.xlabel('Importance') plt.ylabel('Feature') x = 100/len(XX.columns) plt.axvline(x,color = 'red', linestyle = '--') plt.show()
Этот блок кода отобразит важность свойств, а также срединную линию. Существует множество способов определения значимости свойств в области машинного обучения, например:
- Значимость на основе дерева: измеряет уменьшение засоренности (например, индекс Джини) в деревьях решений или ансамблях, таких как Random Forest и XGBoost.
- Значимость перестановки: оценивает падение производительности при перестановке значений свойства.
- Значения SHAP: рассчитывает вклад свойства в прогнозы на основе значений Шепли.
- Величина коэффициента: использует абсолютное значение коэффициентов в линейных моделях.
В нашем примере мы используем CatBoost — модель на основе дерева решений. Значимость свойств показывает, насколько каждое свойство снижает беспорядок (засоренность) при использовании этого свойства для разделения дерева решений во внутривыборочных данных. Ключевым фактором является понимание, что выбор наиболее значимых свойств в качестве окончательного набора часто может повышать эффективность модели, но не всегда улучшает прогнозируемость по следующим причинам:
- Значимость свойств рассчитывается из внутривыборочных данных без учета данных за пределами выборки.
- Значимость свойства зависит от других рассматриваемых свойств. Если большинству выбранных вами свойств не хватает прогностических возможностей, исключение самых слабых из них не поможет.
- Значимость отражает, насколько эффективно свойство разделяет дерево, а не обязательно то, насколько оно критично для окончательного решения.
Эти идеи пришли мне в голову, когда я неожиданно обнаружил, что отбор наименее важных свойств на самом деле повышает точность за пределами выборки. Но в целом выбор наиболее значимых свойств и отбрасывание наименее значимых позволяет облегчить модель и, вероятно, повышает общую точность.
from onnx.helper import get_attribute_value import onnxruntime as rt from skl2onnx import convert_sklearn, update_registered_converter from skl2onnx.common.shape_calculator import ( calculate_linear_classifier_output_shapes, ) # noqa from skl2onnx.common.data_types import ( FloatTensorType, Int64TensorType, guess_tensor_type, ) from skl2onnx._parse import _apply_zipmap, _get_sklearn_operator_name from catboost import CatBoostClassifier from catboost.utils import convert_to_onnx_object def skl2onnx_parser_castboost_classifier(scope, model, inputs, custom_parsers=None): options = scope.get_options(model, dict(zipmap=True)) no_zipmap = isinstance(options["zipmap"], bool) and not options["zipmap"] alias = _get_sklearn_operator_name(type(model)) this_operator = scope.declare_local_operator(alias, model) this_operator.inputs = inputs label_variable = scope.declare_local_variable("label", Int64TensorType()) prob_dtype = guess_tensor_type(inputs[0].type) probability_tensor_variable = scope.declare_local_variable( "probabilities", prob_dtype ) this_operator.outputs.append(label_variable) this_operator.outputs.append(probability_tensor_variable) probability_tensor = this_operator.outputs if no_zipmap: return probability_tensor return _apply_zipmap( options["zipmap"], scope, model, inputs[0].type, probability_tensor ) def skl2onnx_convert_catboost(scope, operator, container): """ CatBoost returns an ONNX graph with a single node. This function adds it to the main graph. """ onx = convert_to_onnx_object(operator.raw_operator) opsets = {d.domain: d.version for d in onx.opset_import} if "" in opsets and opsets[""] >= container.target_opset: raise RuntimeError("CatBoost uses an opset more recent than the target one.") if len(onx.graph.initializer) > 0 or len(onx.graph.sparse_initializer) > 0: raise NotImplementedError( "CatBoost returns a model initializers. This option is not implemented yet." ) if ( len(onx.graph.node) not in (1, 2) or not onx.graph.node[0].op_type.startswith("TreeEnsemble") or (len(onx.graph.node) == 2 and onx.graph.node[1].op_type != "ZipMap") ): types = ", ".join(map(lambda n: n.op_type, onx.graph.node)) raise NotImplementedError( f"CatBoost returns {len(onx.graph.node)} != 1 (types={types}). " f"This option is not implemented yet." ) node = onx.graph.node[0] atts = {} for att in node.attribute: atts[att.name] = get_attribute_value(att) container.add_node( node.op_type, [operator.inputs[0].full_name], [operator.outputs[0].full_name, operator.outputs[1].full_name], op_domain=node.domain, op_version=opsets.get(node.domain, None), **atts, ) update_registered_converter( CatBoostClassifier, "CatBoostCatBoostClassifier", calculate_linear_classifier_output_shapes, skl2onnx_convert_catboost, parser=skl2onnx_parser_castboost_classifier, options={"nocl": [True, False], "zipmap": [True, False, "columns"]}, ) model_onnx = convert_sklearn( model, "catboost", [("input", FloatTensorType([None, X.shape[1]]))], target_opset={"": 12, "ai.onnx.ml": 2}, ) # And save. with open("box2024.onnx", "wb") as f: f.write(model_onnx.SerializeToString())
Наконец, экспортируем файл ONNX и сохраним его в каталоге MQL5/Files.
Теперь вернемся к редактору кода MetaTrader 5 для создания торгового советника.
Нужно просто настроить исходный советник, предназначенный для извлечения данных, импортировав несколько включаемых файлов для обработки модели CatBoost.
#resource "\\Files\\box2024.onnx" as uchar catboost_onnx[] #include <CatOnnx.mqh> CCatBoost cat_boost; string data[1][12]; vector xx; vector prob;
Затем настроим функцию getData() так, чтобы она возвращала вектор.
vector getData(){ double close = iClose(_Symbol,PERIOD_CURRENT,1); double close2 = iClose(_Symbol,PERIOD_CURRENT,2); double close3 = iClose(_Symbol,PERIOD_CURRENT,3); double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close; double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2; double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3; double highDistance = 1000*(close-high)/close; double lowDistance = 1000*(close-low)/close; double boxSize = 1000*(high-low)/close; double adx[]; // Average Directional Movement Index double wilder[]; // Average Directional Movement Index by Welles Wilder double dm[]; // DeMarker double rsi[]; // Relative Strength Index double rvi[]; // Relative Vigor Index double sto[]; // Stochastic Oscillator CopyBuffer(handleAdx, 0, 1, 1, adx); // Average Directional Movement Index CopyBuffer(handleWilder, 0, 1, 1, wilder); // Average Directional Movement Index by Welles Wilder CopyBuffer(handleDm, 0, 1, 1, dm); // DeMarker CopyBuffer(handleRsi, 0, 1, 1, rsi); // Relative Strength Index CopyBuffer(handleRvi, 0, 1, 1, rvi); // Relative Vigor Index CopyBuffer(handleSto, 0, 1, 1, sto); // Stochastic Oscillator data[0][0] = DoubleToString(adx[0], 2); // Average Directional Movement Index data[0][1] = DoubleToString(wilder[0], 2); // Average Directional Movement Index by Welles Wilder data[0][2] = DoubleToString(dm[0], 2); // DeMarker data[0][3] = DoubleToString(rsi[0], 2); // Relative Strength Index data[0][4] = DoubleToString(rvi[0], 2); // Relative Vigor Index data[0][5] = DoubleToString(sto[0], 2); // Stochastic Oscillator data[0][6] = DoubleToString(stationary,2); data[0][7] = DoubleToString(boxSize,2); data[0][8] = DoubleToString(stationary2,2); data[0][9] = DoubleToString(stationary3,2); data[0][10] = DoubleToString(highDistance,2); data[0][11] = DoubleToString(lowDistance,2); vector features(12); for(int i = 0; i < 12; i++) { features[i] = StringToDouble(data[0][i]); } return features; }
Окончательная торговая логика в функции OnTick() будет выглядеть следующим образом:
void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars; boxFormed = false; bool NotInPosition = true; lastClose = iClose(_Symbol, PERIOD_CURRENT, 1); lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2); for(int i = 0; i<PositionsTotal(); i++){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;} /*count++; if(count >=countMax){ trade.PositionClose(pos); count = 0;} }}*/ DetectDarvasBox(lookBack,checkBar); if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low))){ xx = getData(); prob = cat_boost.predict_proba(xx); if(prob[1]>threshold)executeBuy(); if(prob[0]>threshold)executeSell(); } } }
В логике сигнала она сначала проверяет, что в данный момент нет открытых позиций, чтобы обеспечивать возможность совершения только одной сделки за раз. Затем она определяет оба предельных уровня диапазона на предмет наличия прорыва. После этого она вызывает функцию getData() для получения вектора свойств. Этот вектор передается в модель CatBoost в качестве входных данных, и модель выводит достоверность прогноза для каждого результата в массив вероятностей. В зависимости от уровня достоверности для каждого результата мы размещаем сделку, причем ставим на спрогнозированный результат. По сути, мы используем модель для генерации сигналов на покупку или продажу.
Мы провели тестирование на исторических данных в тестере стратегий MetaTrader 5, используя внутривыборочные данные за период с 2020 по 2024 год, чтобы подтвердить, что наши обучающие данные не содержат ошибок и что свойства и результаты объединяются корректно. Если все верно, кривая капитала должна выглядеть практически идеально, вот так:
Затем проводим тестирование на истории за пределами выборки за период с 2024 по 2025 год, чтобы проверить, является ли стратегия прибыльной в самое последнее время. Установим порог на 0.7, чтобы модель открывала сделку в каком-либо направлении только в том случае, если уровень достоверности достижения тейк-профита в этом направлении составляет 70% и выше, исходя из обучающих данных.
Видим, что модель показала в первой половине года исключительно хорошие результаты, но с течением времени ее эффективность начала снижаться. Это характерно для моделей машинного обучения, поскольку полученное на основе прошлых данных преимущество часто оказывается временным и имеет тенденцию ослабевать со временем. Это говорит о том, что меньшее соотношение тестовых данных к обучающим может оказаться более эффективным для будущих реализаций в реальной торговле. В целом, модель демонстрирует некоторую прогнозируемость, поскольку остается прибыльной даже после учета торговых издержек.
Непрерывный сигнал
В алгоритмической торговле трейдеры обычно придерживаются простого метода использования дискретных сигналов — либо покупают, либо продают с фиксированным риском по каждой сделке. Это упрощает работу и позволяет лучше анализировать эффективность стратегии. Некоторые трейдеры попытались усовершенствовать этот метод дискретных сигналов, корректируя риск сделки в зависимости от того, насколько строго выполняются условия стратегии. Непрерывные сигналы продолжают развивать этот аддитивный подход, применяя его к условиям более абстрактных стратегий и устанавливая уровень риска между нулем и единицей.
Основная идея заключается в том, что не все соответствующие критериям входа сделки равноценны. У некоторых из них, по-видимому, выше вероятность успешного завершения, поскольку их сигналы более сильные и основаны на нелинейных правилах, привязанных к стратегии. Это можно рассматривать как инструмент управления рисками — повышайте ставки, когда достоверность высока, и снижайте их, когда сделка выглядит менее перспективной, даже если у нее все еще сохраняется положительная ожидаемая доходность. Однако нам следует помнить, что это добавляет к эффективности стратегии еще один фактор и что смещение прогноза и риски переобучения по-прежнему остаются проблематичными, если мы не будем осторожны при реализации.
Для применения этой концепции в нашем торговом советнике нам сначала понадобится настроить функции покупки и продажи, чтобы рассчитать размер лота на основе риска, которому мы готовы подвергнуться в случае достижения стоп-лосса. Функция расчета лота выглядит так:
double calclots(double slpoints, string symbol, double risk) { double balance = AccountInfoDouble(ACCOUNT_BALANCE); double riskAmount = balance* risk / 100; double ticksize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); double tickvalue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE); double lotstep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); double moneyperlotstep = slpoints / ticksize * tickvalue * lotstep; double lots = MathFloor(riskAmount / moneyperlotstep) * lotstep; lots = MathMin(lots, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX)); lots = MathMax(lots, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN)); return lots; }Затем обновим функции покупки и продажи таким образом, чтобы они вызывали эту функцию calclots() и принимали в качестве входных данных фактор риска:
void executeSell(double riskMultiplier) { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); double sl = lastClose*(1+slp); double tp = lastClose*(1-tpp); double lots = 0.1; lots = calclots(slp*lastClose,_Symbol,risks*riskMultiplier); trade.Sell(lots,_Symbol,bid,sl,tp); } void executeBuy(double riskMultiplier) { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); double sl = lastClose*(1-slp); double tp = lastClose*(1+tpp); double lots = 0.1; lots = calclots(slp*lastClose,_Symbol,risks*riskMultiplier); trade.Buy(lots,_Symbol,ask,sl,tp); }Поскольку наша модель машинного обучения уже выводит уровень достоверности, мы можем непосредственно использовать его в качестве входных данных фактора риска. Если мы хотим отрегулировать степень влияния уровня достоверности на риск для каждой сделки, мы можем просто увеличивать или уменьшать степень достоверности по мере необходимости.
if(prob[1]>threshold)executeBuy(prob[1]); if(prob[0]>threshold)executeSell(prob[0]);
Например, если мы хотим усилить значимость различий в уровнях достоверности, мы можем умножить вероятность на саму себя три раза. Это увеличило бы разницу в соотношении вероятностей, сделав влияние уровней достоверности более выраженным.
if(prob[1]>threshold)executeBuy(prob[1]*prob[1]*prob[1]); if(prob[0]>threshold)executeSell(prob[0]*prob[0]*prob[0]);
Теперь попробуем увидеть результат в тесте на исторических данных.
Совершенные сделки остались такими же, как и в версии с дискретным сигналом, но фактор прибыли и коэффициент Шарпа немного улучшились. Это говорит о том, что в данном конкретном сценарии непрерывный сигнал повысил общую эффективность теста за пределами выборки, который свободен от смещения опережающего просмотра, поскольку мы проводили тестирование только один раз. Однако важно отметить, что этот подход превосходит метод фиксированного риса только в том случае, если прогностическая точность модели становится выше при повышении уровня ее достоверности. В ином случае изначальный подход на основе фиксированного риска мог бы оказаться лучше. Кроме того, поскольку мы уменьшили средний размер лота, применив множители риска от нуля до единицы, нам понадобится увеличить значение переменной риска, если хотим добиться такой же общей прибыли, как и раньше.
Проверка на нескольких таймфреймах
Тренировка отдельных моделей машинного обучения, каждая из которых использует разные временные рамки свойств для прогнозирования одного и того же результата, может стать эффективным способом улучшения фильтрации сделок и генерации сигналов. С помощью одной модели, ориентированной на краткосрочные данные, другой — на среднесрочные и, возможно, третьей, ориентированной на долгосрочные тренды, можно получить адаптированные к конкретным условиям сведения, которые в сочетании могут подтвердить прогнозы более надежно, чем отдельная модель. Такой многомодельный подход может повысить достоверность при принятии торговых решений за счет перекрестной проверки сигналов, снижая риск принятия мер на основе шума, характерного для одного таймфрейма, а также поддерживает управление рисками, позволяя взвешивать выходные данные каждой модели для корректировки размера сделки или стоп-сигналов на основе силы согласования.
С другой стороны, такая стратегия может усложнить систему, особенно если прогнозам из нескольких моделей присваивать разные веса. Если не провести тщательную настройку, это может привести к ее собственным предубеждениям и ошибкам. Кроме того, каждая модель может переобучиться для своего конкретного таймфрейма, упуская более широкую динамику рынка, а расхождения между их прогнозами могут создавать путаницу, задерживать принятие решений или отрицательно влиять на достоверность.
Этот подход основывается на двух ключевых предположениях: во-первых, в более высоком таймфрейме не вносится какое-либо смещение прогнозирования (мы должны использовать значение последнего бара, а не текущего), а во-вторых, у модели для более высокого таймфрейма есть собственная возможность прогнозирования (она работает лучше, чем случайное угадывание в тестах за пределами выборки).
Чтобы реализовать это, сначала изменим код в советнике, отвечающем за выборку данных, изменив все таймфреймы, связанные с извлечением свойств, на более высокие, например на 1-часовой. Сюда входят индикаторы, расчеты цен и любые другие используемые свойства.
int OnInit() { trade.SetExpertMagicNumber(Magic); handleAdx = iADX(_Symbol, PERIOD_H1, 14); // Average Directional Movement Index - 3 handleWilder = iADXWilder(_Symbol, PERIOD_H1, 14); // Average Directional Movement Index by Welles Wilder - 3 handleDm = iDeMarker(_Symbol, PERIOD_H1, 14); // DeMarker - 1 handleRsi = iRSI(_Symbol, PERIOD_H1, 14, PRICE_CLOSE); // Relative Strength Index - 1 handleRvi = iRVI(_Symbol, PERIOD_H1, 10); // Relative Vigor Index - 2 handleSto = iStochastic(_Symbol, PERIOD_H1, 5, 3, 3, MODE_SMA, STO_LOWHIGH); // Stochastic Oscillator - 2 return(INIT_SUCCEEDED); } void getData() { double close = iClose(_Symbol, PERIOD_H1, 1); double close2 = iClose(_Symbol, PERIOD_H1, 2); double close3 = iClose(_Symbol, PERIOD_H1, 3); double stationary = 1000 * (close - iOpen(_Symbol, PERIOD_H1, 1)) / close; double stationary2 = 1000 * (close2 - iOpen(_Symbol, PERIOD_H1, 2)) / close2; double stationary3 = 1000 * (close3 - iOpen(_Symbol, PERIOD_H1, 3)) / close3; }
После этого следуем теми же этапами, что и раньше: извлекаем данные, обучаем модель и экспортируем ее, именно так, как мы обсуждали ранее.
Затем в торговом советнике создаем вторую функцию для извлечения входных данных свойств. Передадим эту функцию во вторую модель машинного обучения, которую импортировали для получения на выходе уровня достоверности.
vector getData2() { double close = iClose(_Symbol, PERIOD_H1, 1); double close2 = iClose(_Symbol, PERIOD_H1, 2); double close3 = iClose(_Symbol, PERIOD_H1, 3); double stationary = 1000 * (close - iOpen(_Symbol, PERIOD_H1, 1)) / close; double stationary2 = 1000 * (close2 - iOpen(_Symbol, PERIOD_H1, 2)) / close2; double stationary3 = 1000 * (close3 - iOpen(_Symbol, PERIOD_H1, 3)) / close3; double highDistance = 1000 * (close - high) / close; double lowDistance = 1000 * (close - low) / close; double boxSize = 1000 * (high - low) / close; double adx[]; // Average Directional Movement Index double wilder[]; // Average Directional Movement Index by Welles Wilder double dm[]; // DeMarker double rsi[]; // Relative Strength Index double rvi[]; // Relative Vigor Index double sto[]; // Stochastic Oscillator CopyBuffer(handleAdx, 0, 1, 1, adx); // Average Directional Movement Index CopyBuffer(handleWilder, 0, 1, 1, wilder); // Average Directional Movement Index by Welles Wilder CopyBuffer(handleDm, 0, 1, 1, dm); // DeMarker CopyBuffer(handleRsi, 0, 1, 1, rsi); // Relative Strength Index CopyBuffer(handleRvi, 0, 1, 1, rvi); // Relative Vigor Index CopyBuffer(handleSto, 0, 1, 1, sto); // Stochastic Oscillator data[0][0] = DoubleToString(adx[0], 2); // Average Directional Movement Index data[0][1] = DoubleToString(wilder[0], 2); // Average Directional Movement Index by Welles Wilder data[0][2] = DoubleToString(dm[0], 2); // DeMarker data[0][3] = DoubleToString(rsi[0], 2); // Relative Strength Index data[0][4] = DoubleToString(rvi[0], 2); // Relative Vigor Index data[0][5] = DoubleToString(sto[0], 2); // Stochastic Oscillator data[0][6] = DoubleToString(stationary, 2); data[0][7] = DoubleToString(boxSize, 2); data[0][8] = DoubleToString(stationary2, 2); data[0][9] = DoubleToString(stationary3, 2); data[0][10] = DoubleToString(highDistance, 2); data[0][11] = DoubleToString(lowDistance, 2); vector features(12); for(int i = 0; i < 12; i++) { features[i] = StringToDouble(data[0][i]); } return features; }
Предположим, что мы хотим присвоить результатам обеих моделей одинаковый вес. Тогда мы просто возьмем среднее значение их результатов и рассмотрим его как единый результат, который использовали ранее.
if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low))){ xx = getData(); xx2 = getData2(); prob = cat_boost.predict_proba(xx); prob2 = cat_boost.predict_proba(xx2); double probability_buy = (prob[1]+prob2[1])/2; double probability_sell = (prob[0]+prob2[0])/2; if(probability_buy>threshold)executeBuy(probability_buy); if(probability_sell>threshold)executeSell(probability_sell); } }
Рассчитав эти две переменные, как указано выше, мы теперь можем объединить их в единый уровень достоверности и использовать его для проверки, следуя тому же подходу, который мы использовали ранее.
Заключение
В этой статье мы впервые рассмотрели идею использования модели машинного обучения в качестве генератора сигналов вместо фильтра, продемонстрированную на примере стратегии прорыва коридора Дарваса. Мы совершили краткий экскурс по процессу обучения модели и обсудили значимость пороговых значений уровня достоверности и ценность свойств. Затем мы ввели понятие непрерывных сигналов и сравнили их эффективность с эффективностью дискретных сигналов. Мы обнаружили, что в этом примере непрерывные сигналы повышали качество и производительность тестирования на исторических данных, поскольку модель, как правило, имела более высокую точность прогнозирования по мере повышения уровня достоверности. Наконец, мы затронули концепцию использования нескольких моделей машинного обучения, обученных на разных таймфреймах, для совместной проверки сигналов.
В целом, целью данной статьи является представление нетрадиционных идей применения моделей машинного обучения при контролируемом обучении для торговли с помощью CTA (Commodity Trading Advisor). Кроме того, цель ее не предполагает однозначно утверждать, какой именно подход работает лучше всего, поскольку все зависит от конкретной ситуации, а скорее вдохновить читателей мыслить творчески и расширять простые исходные концепции. В конце концов, ничто не является абсолютно новым — инновации часто происходят из объединения существующих идей для создания чего-то необычного.
Таблица файлов
Название файла | Использование файла |
---|---|
Darvas_Box.ipynb | Файл Jupyter Notebook для тренировки модели машинного обучения |
Darvas Box Data.mq5 | Советник для извлечения данных с целью обучения модели |
Darvas Box EA.mq5 | Торговый советник в статье |
CatOnnx.mqh | Включаемый файл для обработки модели CatBoost model |
FileCSV.mqh | Включаемый файл для сохранения данных в CSV |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17466
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Спасибо за напоминание. Часть pip install была проигнорирована, но пользователи должны установить соответствующую библиотеку, если они этого еще не сделали.
Ваша ошибка может быть вызвана тем, что размерность, используемая при обучении модели, отличается от размерности, используемой в вашем советнике. Например, если вы обучали модель с 5 признаками, то и в советнике вы должны вводить 5 признаков, а не 4 или 6. Более подробный обзор приведен в этой статье по ссылке. Надеюсь, это поможет. Если нет, пожалуйста, предоставьте больше информации.