
Применение модели машинного обучения CatBoost в качестве фильтра для трендовых стратегий
Введение
CatBoost – это эффективная модель машинного обучения на основе деревьев, которая специализируется на принятии решений на основе статических признаков. Другие модели на основе деревьев, такие как XGBoost и Random Forest, обладают схожими характеристиками в плане надежности, интерпретируемости и способности работать со сложными паттернами. Эти модели имеют широкий спектр применения: от анализа признаков до управления рисками.
В данной статье мы пройдемся по процедуре использования обученной модели CatBoost в качестве фильтра для классической трендовой стратегии на основе пересечения скользящих средних. Цель данной статьи заключается в том, чтобы дать представление о процессе разработки стратегии, а также предоставить решения задач, с которыми можно при этом столкнуться. Я представлю свой рабочий процесс по выборке данных из MetaTrader 5, обучению модели машинного обучения на языке Python и обратной интеграции в советники MetaTrader 5. К концу данной статьи мы проверим стратегию посредством статистического тестирования и обсудим планы на будущее, основанные на текущем подходе.
Интуиция
В нашей отрасли, когда речь заходит о разработке стратегий для советников по торговле товарными активами (CTA, Commodity Trading Advisor), правило большого пальца заключается в том, что лучше всего, когда за каждой идеей для стратегии стоит четкое, интуитивное объяснение. Именно в таком ключе люди в первую очередь размышляют об идеях для стратегий, не говоря о том, что это позволяет также избежать переобучения. Данный тезис применим даже при работе с моделями машинного обучения. Мы попытаемся объяснить интуитивную основу предлагаемой идеи.
Почему это может сработать:
Модель CatBoost создает деревья решений, которые принимают на вход признаки, а затем выдают вероятность каждого из исходов. В данном случае мы обучаем модель только на двоичных исходах (где 1 – это выигрыш, а 0 – потеря). Модель будет изменять правила в деревьях решений таким образом, чтобы минимизировать функцию потерь в обучающем наборе данных. Если модель продемонстрирует определенный уровень предсказуемости на результатах тестирования за пределами выборки, мы можем считать, что ее использование отфильтровывает сделки, которые имеют низкую вероятность выигрыша, что может, в свою очередь, повысить общую прибыльность.
Розничные трейдеры, такие как мы с вами, будут ожидать с долей реализма, что модели, которые мы обучаем, не будут оракулами, а скорее будут лишь немногим лучше случайного блуждания. Существует множество способов усовершенствовать точность модели, которые я рассмотрю позже, но тем не менее, это отличная попытка добиться небольшого улучшения.
Оптимизация базовой стратегии
Мы уже знаем из раздела выше, что мы можем лишь ожидать, что модель слегка повысит свои показатели, поэтому крайне важно, чтобы базовая стратегия уже показывала некоторую доходность.
Стратегия также должна быть способна генерировать большое количество образцов, поскольку:
- Модель будет отфильтровывать часть сделок, и мы хотим убедиться, что останется достаточно образцов, чтобы продемонстрировать статистическую значимость согласно закону больших чисел.
- Нам нужно достаточно образцов для того, чтобы модель продолжала обучаться и эффективно минимизировать функцию потерь для данных внутри выборки.
Мы используем трендовую стратегию, подтвердившую свою эффективность на истории, при которой сделки совершаются, когда две скользящие средние с разными периодами пересекаются. Выходить из сделок мы будем, когда цена зайдет на обратную сторону скользящей средней, т.е. будем следовать за трендом. Ниже приведен код на MQL5 для советника с этой стратегией.
#include <Trade/Trade.mqh> //XAU - 1h. CTrade trade; input ENUM_TIMEFRAMES TF = PERIOD_CURRENT; input ENUM_MA_METHOD MaMethod = MODE_SMA; input ENUM_APPLIED_PRICE MaAppPrice = PRICE_CLOSE; input int MaPeriodsFast = 15; input int MaPeriodsSlow = 25; input int MaPeriods = 200; input double lott = 0.01; ulong buypos = 0, sellpos = 0; input int Magic = 0; int barsTotal = 0; int handleMaFast; int handleMaSlow; int handleMa; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(Magic); handleMaFast =iMA(_Symbol,TF,MaPeriodsFast,0,MaMethod,MaAppPrice); handleMaSlow =iMA(_Symbol,TF,MaPeriodsSlow,0,MaMethod,MaAppPrice); handleMa = iMA(_Symbol,TF,MaPeriods,0,MaMethod,MaAppPrice); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); //Beware, the last element of the buffer list is the most recent data, not [0] if (barsTotal!= bars){ barsTotal = bars; double maFast[]; double maSlow[]; double ma[]; CopyBuffer(handleMaFast,BASE_LINE,1,2,maFast); CopyBuffer(handleMaSlow,BASE_LINE,1,2,maSlow); CopyBuffer(handleMa,0,1,1,ma); double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double lastClose = iClose(_Symbol, PERIOD_CURRENT, 1); //The order below matters if(buypos>0&& lastClose<maSlow[1]) trade.PositionClose(buypos); if(sellpos>0 &&lastClose>maSlow[1])trade.PositionClose(sellpos); if (maFast[1]>maSlow[1]&&maFast[0]<maSlow[0]&&buypos ==sellpos)executeBuy(); if(maFast[1]<maSlow[1]&&maFast[0]>maSlow[0]&&sellpos ==buypos) executeSell(); if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = 0; } if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = 0; } } } //+------------------------------------------------------------------+ //| Expert trade transaction handling function | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { if (trans.type == TRADE_TRANSACTION_ORDER_ADD) { COrderInfo order; if (order.Select(trans.order)) { if (order.Magic() == Magic) { if (order.OrderType() == ORDER_TYPE_BUY) { buypos = order.Ticket(); } else if (order.OrderType() == ORDER_TYPE_SELL) { sellpos = order.Ticket(); } } } } } //+------------------------------------------------------------------+ //| Execute sell trade function | //+------------------------------------------------------------------+ void executeSell() { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); trade.Sell(lott,_Symbol,bid); sellpos = trade.ResultOrder(); } //+------------------------------------------------------------------+ //| Execute buy trade function | //+------------------------------------------------------------------+ void executeBuy() { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); trade.Buy(lott,_Symbol,ask); buypos = trade.ResultOrder(); }
Для проверки вашей базовой стратегии следует учесть следующее:
- Достаточный размер выборки. Частота зависит от вашего таймфрейма и ограничений на сигналы, но в общей сложности я бы предложил от 1000 до 10000 сделок. Каждая сделка – это образец.
- Стратегия уже должна показывать некоторую прибыльность, пусть и небольшую. Я бы сказал, что достаточно будет профит фактора от 1 до 1,15. Поскольку тестер стратегий MetaTrader 5 уже учитывает спреды, профит фактор, равный единице, означает, что стратегия уже имеет статистическое преимущество. Если профит фактор превышает 1,15, вероятнее всего, стратегия достаточно хороша и сама по себе, и возможно вам не потребуются дополнительные фильтры для улучшения сложности.
- У базовой стратегии не должно быть слишком много параметров. Лучше, когда базовая стратегия простая, поскольку использование модели машинного обучения в качестве фильтра уже добавляет вашей стратегии достаточно сложности. Чем меньше фильтр, тем меньше вероятность переобучения.
Вот что я сделал для оптимизации стратегии:
- Поиск подходящего таймфрейма. После запуска кода на другом таймфрейме я обнаружил, что стратегия лучше всего работает на более крупном таймфрейме, но для генерирования достаточного количества образцов я в конечном итоге остался на часовом.
- Оптимизация параметров. Я оптимизировал период медленной скользящей средней и период быстрой скользящей средней в шаге 5 и получил настройки, представленные в коде выше.
- Я попытался добавить правило, при котором вход должен происходить выше скользящей средней с определенным периодом, что будет сигнализировать о том, что уже сформировался тренд в соответствующем направлении. (Важно отметить, что добавление фильтров также должно иметь интуитивное объяснение и подтверждать эту гипотезу, чтобы проверить ее без обращения к данным.) Но в конечном итоге оказалось, что это не сильно улучшило показатели, поэтому от этой идеи я отказался, чтобы избежать избыточного усложнения.
В итоге тестирование показало следующие результаты на часовом таймфрейме XAUUSD за период с 1/1/2004 по 1/11/2024:
Выборка данных
Для обучения модели нам необходимы значения признаков по каждой сделке, и нам нужно знать исход каждой сделки. Самый эффективный и надежный способ, которым я пользуюсь, заключается в написании советника, который сохраняет все соответствующие признаки в двумерный массив, а для получения выходных данных мы просто экспортируем отчет о торговле, полученный в результате ретроспективного тестирования.
Во-первых, чтобы получить выходные данные, мы можем просто перейти к ретроспективному тестированию и, кликнув правой кнопкой мыши, выбрать отчет и открыть XML, как показано ниже.
Затем, чтобы сконвертировать double-массив в формат CSV, мы воспользуемся классом CFileCSV, о котором есть статья.
Мы возьмем за основу скрипт нашей базовой стратегии и выполним следующие шаги:
1. Добавим mqh-файл и создадим объект класса.
#include <FileCSV.mqh>
CFileCSV csvFile;
2. Объявим имя сохраняемого файла и заголовки, среди которых будет "index" и названия всех остальных признаков. Здесь "index" используется только для обновления массива в процессе работы тестера и позже будет отброшен, когда мы перейдем к Python.
string fileName = "ML.csv"; string headers[] = { "Index", "Accelerator Oscillator", "Average Directional Movement Index", "Average Directional Movement Index by Welles Wilder", "Average True Range", "Bears Power", "Bulls Power", "Commodity Channel Index", "Chaikin Oscillator", "DeMarker", "Force Index", "Gator", "Market Facilitation Index", "Momentum", "Money Flow Index", "Moving Average of Oscillator", "MACD", "Relative Strength Index", "Relative Vigor Index", "Standard Deviation", "Stochastic Oscillator", "Williams' Percent Range", "Variable Index Dynamic Average", "Volume", "Hour", "Stationary" }; string data[10000][26]; int indexx = 0; vector xx;
3. Пишем функцию getData(), которая рассчитывает все значения признаков и сохраняет их в глобальный массив. В нашем случае мы используем в качестве признаков время, осцилляторы и стационарную цену. Данная функция будет вызываться каждый раз, когда появляется торговый сигнал, то есть одновременно с вашими сделками. О выборе признаков я расскажу позже.
//+------------------------------------------------------------------+ //| Execute get data function | //+------------------------------------------------------------------+ vector getData(){ //23 oscillators double ac[]; // Accelerator Oscillator double adx[]; // Average Directional Movement Index double wilder[]; // Average Directional Movement Index by Welles Wilder double atr[]; // Average True Range double bep[]; // Bears Power double bup[]; // Bulls Power double cci[]; // Commodity Channel Index double ck[]; // Chaikin Oscillator double dm[]; // DeMarker double f[]; // Force Index double g[]; // Gator double bwmfi[]; // Market Facilitation Index double m[]; // Momentum double mfi[]; // Money Flow Index double oma[]; // Moving Average of Oscillator double macd[]; // Moving Averages Convergence/Divergence double rsi[]; // Relative Strength Index double rvi[]; // Relative Vigor Index double std[]; // Standard Deviation double sto[]; // Stochastic Oscillator double wpr[]; // Williams' Percent Range double vidya[]; // Variable Index Dynamic Average double v[]; // Volume CopyBuffer(handleAc, 0, 1, 1, ac); // Accelerator 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(handleAtr, 0, 1, 1, atr); // Average True Range CopyBuffer(handleBep, 0, 1, 1, bep); // Bears Power CopyBuffer(handleBup, 0, 1, 1, bup); // Bulls Power CopyBuffer(handleCci, 0, 1, 1, cci); // Commodity Channel Index CopyBuffer(handleCk, 0, 1, 1, ck); // Chaikin Oscillator CopyBuffer(handleDm, 0, 1, 1, dm); // DeMarker CopyBuffer(handleF, 0, 1, 1, f); // Force Index CopyBuffer(handleG, 0, 1, 1, g); // Gator CopyBuffer(handleBwmfi, 0, 1, 1, bwmfi); // Market Facilitation Index CopyBuffer(handleM, 0, 1, 1, m); // Momentum CopyBuffer(handleMfi, 0, 1, 1, mfi); // Money Flow Index CopyBuffer(handleOma, 0, 1, 1, oma); // Moving Average of Oscillator CopyBuffer(handleMacd, 0, 1, 1, macd); // Moving Averages Convergence/Divergence CopyBuffer(handleRsi, 0, 1, 1, rsi); // Relative Strength Index CopyBuffer(handleRvi, 0, 1, 1, rvi); // Relative Vigor Index CopyBuffer(handleStd, 0, 1, 1, std); // Standard Deviation CopyBuffer(handleSto, 0, 1, 1, sto); // Stochastic Oscillator CopyBuffer(handleWpr, 0, 1, 1, wpr); // Williams' Percent Range CopyBuffer(handleVidya, 0, 1, 1, vidya); // Variable Index Dynamic Average CopyBuffer(handleV, 0, 1, 1, v); // Volume //2 means 2 decimal places data[indexx][0] = IntegerToString(indexx); data[indexx][1] = DoubleToString(ac[0], 2); // Accelerator Oscillator data[indexx][2] = DoubleToString(adx[0], 2); // Average Directional Movement Index data[indexx][3] = DoubleToString(wilder[0], 2); // Average Directional Movement Index by Welles Wilder data[indexx][4] = DoubleToString(atr[0], 2); // Average True Range data[indexx][5] = DoubleToString(bep[0], 2); // Bears Power data[indexx][6] = DoubleToString(bup[0], 2); // Bulls Power data[indexx][7] = DoubleToString(cci[0], 2); // Commodity Channel Index data[indexx][8] = DoubleToString(ck[0], 2); // Chaikin Oscillator data[indexx][9] = DoubleToString(dm[0], 2); // DeMarker data[indexx][10] = DoubleToString(f[0], 2); // Force Index data[indexx][11] = DoubleToString(g[0], 2); // Gator data[indexx][12] = DoubleToString(bwmfi[0], 2); // Market Facilitation Index data[indexx][13] = DoubleToString(m[0], 2); // Momentum data[indexx][14] = DoubleToString(mfi[0], 2); // Money Flow Index data[indexx][15] = DoubleToString(oma[0], 2); // Moving Average of Oscillator data[indexx][16] = DoubleToString(macd[0], 2); // Moving Averages Convergence/Divergence data[indexx][17] = DoubleToString(rsi[0], 2); // Relative Strength Index data[indexx][18] = DoubleToString(rvi[0], 2); // Relative Vigor Index data[indexx][19] = DoubleToString(std[0], 2); // Standard Deviation data[indexx][20] = DoubleToString(sto[0], 2); // Stochastic Oscillator data[indexx][21] = DoubleToString(wpr[0], 2); // Williams' Percent Range data[indexx][22] = DoubleToString(vidya[0], 2); // Variable Index Dynamic Average data[indexx][23] = DoubleToString(v[0], 2); // Volume datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; data[indexx][24]= IntegerToString(currentHour); double close = iClose(_Symbol,PERIOD_CURRENT,1); double open = iOpen(_Symbol,PERIOD_CURRENT,1); double stationary = MathAbs((close-open)/close)*100; data[indexx][25] = DoubleToString(stationary,2); vector features(26); for(int i = 1; i < 26; i++) { features[i] = StringToDouble(data[indexx][i]); } //A lot of the times positions may not open due to error, make sure you don't increase index blindly if(PositionsTotal()>0) indexx++; return features; }
Заметьте, что мы добавили сюда проверку.
if(PositionsTotal()>0) indexx++;
Это связано с тем, что когда возникает торговый сигнал, он может не привести к открытию сделки, поскольку советник работает в часы, когда рынок закрыт, при этом тестер не будет совершать никаких сделок.
4. Мы сохраняем файл при вызове функции OnDeInit(), что означает завершение теста.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ 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!"); } }
Запустите этот советник в тестере стратегий, после чего в директории /Tester/Agent-sth000 должен появиться ваш csv-файл.
Очистка и корректировка данных
Теперь у нас есть два файла с данными, но остается еще множество нерешенных фундаментальных проблем.
1. Отчет по результатам ретроспективного тестирования выглядит запутанно и представлен в формате .xlsx. По каждой из сделок мы хотим лишь узнать, выиграли мы или нет.
Сначала мы извлечем строки, в которых отображается только результат сделок. Возможно вам нужно будет прокрутить вниз ваш XLSX-файл, пока вы не найдете что-то наподобие этого:
Запомните номер строки и учтите его в следующем коде на Python:
import pandas as pd # Replace 'your_file.xlsx' with the path to your file input_file = 'ML2.xlsx' # Load the Excel file and skip the first {skiprows} rows df = pd.read_excel(input_file, skiprows=10757) # Save the extracted content to a CSV file output_file = 'extracted_content.csv' df.to_csv(output_file, index=False) print(f"Content has been saved to {output_file}.")
Затем мы применим эти извлеченные данные в нижеследующем коде, чтобы получить обработанную бинарную таблицу. В ней прибыльные сделки будут обозначены единицами, а убыточные – нулями.
import pandas as pd # Load the CSV file file_path = 'extracted_content.csv' # Update with the correct file path if needed data = pd.read_csv(file_path) # Select the 'profit' column (assumed to be 'Unnamed: 10') and filter rows as per your instructions profit_data = data["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 # Save the processed data to a new CSV file with index output_csv_path = 'processed_bin.csv' profit_data.to_csv(output_csv_path, index=True, header=['bin']) print(f"Processed data saved to {output_csv_path}")
Результат должен выглядеть примерно так:
bin | |
---|---|
0 | 1 |
1 | 0 |
2 | 1 |
3 | 0 |
4 | 0 |
5 | 1 |
Учтите, что если все значения нулевые, это может быть связано с тем, что вы указали неверные строки для старта. Проверьте, является ваша стартовая строка четной или нечетной и внесите соответствующие изменения в код на Python.
2. Данные признаков всегда строковые из-за класса CFileCSV, они склеены в один столбец и лишь разделены запятыми.
Вам поможет следующий код на Python:
import pandas as pd # Load the CSV file with semicolon separator file_path = 'ML.csv' data = pd.read_csv(file_path, sep=';') # Drop rows with any missing or incomplete values data.dropna(inplace=True) # Drop any duplicate rows if present data.drop_duplicates(inplace=True) # Convert non-numeric columns to numerical format for col in data.columns: if data[col].dtype == 'object': # Convert categorical to numerical using label encoding data[col] = data[col].astype('category').cat.codes # Ensure all remaining columns are numeric and cleanly formatted for CatBoost data = data.apply(pd.to_numeric, errors='coerce') data.dropna(inplace=True) # Drop any rows that might still contain NaNs after conversion # Save the cleaned data to a new file in CatBoost-friendly format output_file_path = 'Cleaned.csv' data.to_csv(output_file_path, index=False) print(f"Data cleaned and saved to {output_file_path}")
Наконец, мы используем этот код, чтобы слить два файла в один, чтобы в будущем можно было легко обращаться к ним как к единому блоку данных.
import pandas as pd # Load the two CSV files file1_path = 'processed_bin.csv' # Update with the correct file path if needed file2_path = 'Cleaned.csv' # Update with the correct file path if needed data1 = pd.read_csv(file1_path, index_col=0) # Load first file with index data2 = pd.read_csv(file2_path, index_col=0) # Load second file with index # Merge the two DataFrames on the index merged_data = pd.merge(data1, 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}")
Чтобы подтвердить корректность слияния двух файлов с данными, мы можем проверить три CSV-файла, которые мы только что сгенерировали, и посмотреть, одинаковый ли в них конечный индекс. Если да, скорее всего, у нас все в порядке.
Обучение модели
Мы не будем слишком глубоко погружаться в технические объяснения всех аспектов машинного обучения. Тем не менее, я настоятельно рекомендую вам ознакомиться с книгой Advances in Financial Machine Learning (автор Маркос Лопез де Прадо), если вы интересуетесь ML-трейдингом в целом.
Наша цель в данном разделе ясна и понятна.
Во-первых, мы воспользуемся библиотекой pandas для чтения данных после слияния и примем бинарный столбец за ось Y, а другой – за ось X.
data = pd.read_csv("merged_data.csv",index_col=0) XX = data.drop(columns=['bin']) yy = data['bin'] y = yy.values X = XX.values
Затем мы разделим данные 80% на 20% для обучения и тестирования, соответственно.
Затем проведем обучение. Сведения о каждом параметре классификатора задокументированы на сайте CatBoost.
from catboost import CatBoostClassifier from sklearn.ensemble import BaggingClassifier # Define the CatBoost model with initial parameters catboost_clf = CatBoostClassifier( class_weights=[10, 1], #more weights to 1 class cuz there's less correct cases iterations=20000, # 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 random_seed=RANDOM_STATE, verbose=1000, # Suppress output (set to a positive number if you want to see training progress) ) fit = catboost_clf.fit(X_train, y_train)
Сохраняем файл с расширением .cbm.
catboost_clf.save_model('catboost_test.cbm')
На этом, увы, еще не все. MetaTrader 5 поддерживает только модель формата ONNX, поэтому воспользуемся следующим кодом из соответствующей статьи, чтобы преобразовать модель в формат ONNX.
model_onnx = convert_sklearn( model, "catboost", [("input", FloatTensorType([None, X.shape[1]]))], target_opset={"": 12, "ai.onnx.ml": 2}, ) # And save. with open("CatBoost_test.onnx", "wb") as f: f.write(model_onnx.SerializeToString())
Статистическое тестирование
После получения файла .onnx мы перетаскиваем его в папку MQL5/Files. Теперь мы возьмем за основу советник, который мы ранее использовали для выборки данных. Опять же, в данной статье уже подробно описана процедура инициализации модели .onnx в советниках, я лишь сделаю акцент на том, как мы меняем критерии входа в сделку.
if (maFast[1]>maSlow[1]&&maFast[0]<maSlow[0]&&sellpos == buypos){ xx= getData(); prob = cat_boost.predict_proba(xx); if (prob[1]<max&&prob[1]>min)executeBuy(); } if(maFast[1]<maSlow[1]&&maFast[0]>maSlow[0]&&sellpos == buypos){ xx= getData(); prob = cat_boost.predict_proba(xx); Print(prob); if(prob[1]<max&&prob[1]>min)executeSell(); }
Здесь мы вызываем функцию getData() для сохранения векторной информации в переменную xx, затем мы возвращаем вероятность успеха согласно модели. Мы добавили вызов Print, чтобы наглядно понимать, каков будет примерный диапазон вероятностей. Для трендовой стратегии, ввиду ее низкой точности и высокого соотношения прибыли к риску для каждой сделки, мы обычно видим, что модель выдает вероятность менее 0,5.
Добавляем пороговое значение, чтобы отфильтровывать сделки с низкой вероятностью успеха, и на этом написание кода завершено. Теперь перейдем к тестированию.
Помните, что мы разделили данные в соотношении 8 на 2? Теперь мы проведем тестирование за пределами выборки на данных, не представленных для обучения, то есть примерно за период с 1/1/2021 по 1/11/2024.
Сначала запустим тестирование на данных выборки с порогом вероятности от 0,05, чтобы убедиться, что обучили модель на верных данных. Результат должен быть практически безупречен, как здесь:
Затем в качестве базового варианта мы запустим тестирование вне выборки без нижнего порога. Мы предполагаем, что если повысить пороговое значение, наши результаты значительно превзойдут этот базовый вариант.
Наконец, проведем вневыборочные тесты для анализа паттернов прибыльности в зависимости от различных пороговых значений.
Результаты для порога = 0,05:
Результаты для порога = 0,1:
Результаты для порога = 0,2:
Для порога 0,05 модель отфильтровала примерно половину первоначальной выборки сделок, но это привело к уменьшению доходности. Это может говорить о том, что прогнозирующая модель переобучена, чрезмерно подладилась под торговые паттерны и стала неспособна распознавать паттерны из тестовых наборов, которые похожи на обучающие. В финансовом машинном обучении это распространенная проблема. Тем не менее, при повышении порога с нуля лишь до 0,1, профит фактор постепенно улучшается и превосходит базовый вариант.
При пороге 0,2 модель отфильтровывает примерно 70% первоначальных сделок, при этом оставшиеся сделки в совокупности более прибыльны чем первоначальные. Статистический анализ показывает, что в рамках данного диапазона пороговых значений общая доходность положительно коррелирует с пороговым значением. Это говорит о том, что по мере того, как повышается уверенность модели в сделке, растет и общая ее эффективность, что является благоприятным результатом.
Я провел десятикратную перекрестную проверку на Python, чтобы убедиться, что модель работает с одинаковой точностью.
{'score': array([-0.97148655, -1.25263677, -1.02043177, -1.06770248, -0.97339545, -0.88611439, -0.83877111, -0.95682533, -1.02443847, -1.1385681 ])}
Разница между результатами перекрестных проверок незначительная, что говорит о том, что точность модели остается приблизительно одинаковой при различных комбинациях обучающего и тестового периодов.
Кроме того, при среднем значении логистической функции потерь, равном приблизительно -1, результаты модели можно считать умеренно эффективными.
Чтобы еще больше увеличить точность модели, можно воспользоваться одной из следующих идей:
1. Отбор признаков
Мы составляем график важности признаков, как представлено ниже, и убираем те, которые имеют низкую важность.
Что касается подбора признаков, уместными будут любые, имеющие отношение к рынкам, но обязательно сделайте данные стационарными, поскольку модели на основе деревьев используют правила с фиксированными значениями для обработки входных данных.
2. Настройка по гиперпараметрам
Помните, ранее мы упоминали о параметрах функции классификатора? Мы могли бы написать функцию, которая проходилась бы циклом по сетке значений и тестировала параметры, выявляя среди них наиболее результативные по показателям перекрестных проверок.
3. Подбор модели
Мы можем попробовать другие модели машинного обучения или другие типы значений для предсказания. Многие отмечают, что модели машинного обучения плохо предсказывают цены, но гораздо более компетентны в предсказании волатильности. Кроме того, для предсказания скрытых трендов широко используется скрытая модель Маркова. И то, и другое может служить отличным фильтром для трендовых стратегий.
Буду очень рад читателям, которые решат опробовать эти методы вкупе с моим кодом (прикрепляю ниже) и поделятся со мной обратной связью о том, повысит ли это эффективность модели.
Заключение
В данной статье мы прошлись по всему процессу разработки фильтра на основе модели машинного обучения CatBoost для трендовой стратегии. В процессе мы осветили различные аспекты, о которых следует помнить при исследовании стратегий машинного обучения. И наконец, мы проверили стратегию посредством статистического тестирования и обсудили планы на будущее, основанные на текущем подходе.
Таблица прикрепленных файлов
Имя файла | Назначение |
---|---|
ML-Momentum Data.mq5 | Советник для выборки данных по признакам |
ML-Momentum.mq5 | Конечный исполняемый советник |
CB2.ipynb | Рабочий процесс для обучения и тестирования модели CatBoost |
handleMql5DealReport.py | Извлечение полезных строк из отчета о сделках |
getBinFromMql5.py | Конвертация извлеченного формата в бинарную таблицу |
clean_mql5_csv.py | Очистка CSV-файла с признаками, извлеченного из Mt5 |
merge_data2.py | Слияние признаков и результатов сделок в единый CSV-файл |
OnnxConvert.ipynb | Конвертация модели .cbm в формат .onnx |
Classic Trend Following.mq5 | Советник с базовой стратегией |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/16487
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Здравствуйте. Я играю с CatBoost и дошел до того, что стратегия, обученная на данных (всех) 2024 года, дает >300% прибыли при бэктесте (в MetaTrader) на 2024 году, но плохо работает на других годах. Есть ли у кого-нибудь подобный опыт? Интуитивно кажется, что это перебор, но даже если я тренируюсь с гораздо меньшим количеством итераций (например, 1k), я получаю тот же результат.
Я обучаюсь с ~40-50 признаками на минутных данных, так что получается что-то около 250 000 строк в год. Размер файла .cbm, как правило, получается в 1000 раз больше количества итераций (например, 1000 итераций = 1 МБ, 10 000 итераций = 10 МБ и так далее). Бэктестирование в Metatrader ограничивает меня примерно 100 000 МБ, после чего бэктестер останавливается. Я могу проводить бэктесты на C++ до произвольно больших размеров, но мои результаты в metatrader и C++ дико отличаются.
Я обучаюсь с ~40-50 признаками на минутных данных, так что получается что-то около 250 000 строк в год. Размер файла .cbm, как правило, получается в 1000 раз больше количества итераций (например, 1000 итераций = 1 МБ, 10 000 итераций = 10 МБ и так далее). Бэктестирование в Metatrader ограничивает меня примерно 100 000 МБ, после чего бэктестер останавливается. Я могу проводить бэктесты на C++ до произвольно больших размеров, но мои результаты в metatrader и C++ дико отличаются.
Здравствуйте. Во-первых, бэктестер Metatrader учитывает спреды и комиссии, что может объяснить, почему он отличается от ваших результатов на C++. Во-вторых, на мой взгляд, машинное обучение - это, по сути, процесс оверфиттинга. Существует множество способов уменьшить перебор, таких как ансамбль, отсев и инженерия признаков. Но в конечном счете выборка в выборке всегда намного лучше, чем вне выборки. Использование машинного обучения для прогнозирования финансовых временных рядов - старая проблема. Если вы пытаетесь предсказать доходность (я предполагаю, потому что вы говорите о 250 тыс. рядов), то следует ожидать шума, потому что у вас и других игроков одна и та же цель прогнозирования. В то время как то, что я представил в этой статье, - это метод метамаркировки, где меньше шума, потому что цель прогнозирования сужается до вашей собственной стратегии, но у него будет меньше образцов для обучения, что делает ограничение сложности еще более строгим. Я бы посоветовал снизить ожидания при использовании метода ML и изучить способы уменьшения избыточной подгонки.
Спасибо, что так быстро ответили на тему, которой уже более 6 месяцев. Здесь есть над чем подумать. Я привыкаю к огромному пространству параметров и пытаюсь найти способы уменьшить перебор.
Еще раз спасибо!
Спасибо, что так быстро ответили на тему, которой уже более 6 месяцев. Здесь есть над чем подумать. Я привыкаю к огромному пространству параметров и пытаюсь найти способы уменьшить перебор.
Еще раз спасибо!
Удачи вам в ваших исследованиях!