Преодоление ограничений машинного обучения (Часть 4): Как уменьшить неустранимую ошибку с помощью нескольких горизонтов прогноза
Машинное обучение - это очень широкая область, которую можно изучать и интерпретировать с самых разных точек зрения. Сама эта широта делает освоение этого материала чрезвычайно сложным для любого из нас. В своей серии статей мы рассмотрели некоторые материалы по машинному обучению со статистической точки зрения или с точки зрения линейной алгебры. Однако мы редко уделяем внимание геометрической интерпретации моделей машинного обучения. Традиционно модели машинного обучения описываются как аппроксимирующие функцию, отображающую входные параметры на выходные. Однако с геометрической точки зрения это неполная характеристика.
Что на самом деле делают модели, так это помещают изображения цели в пространство, определяемое входными параметрами, чтобы в будущем они могли попытаться описать цель, используя только эти входные параметры. При этом модель определяет новое многообразие на основе входных данных и делает прогнозы по этому новому многообразию. Но истинная цель находится в своем собственном многообразии. Это смещение создает едва заметную, но неизбежную форму неустранимой ошибки: модель никогда по-настоящему не указывает на цель, а, скорее, может указывать только на некоторую комбинацию входных параметров.
Может помочь мысленный эксперимент. Представьте, что вам дают записи скоростей двух автомобилей и просят оценить, какой из них быстрее. Достаточно просто — до тех пор, пока вы не обнаружите, что скорость одного автомобиля измеряется в милях в минуту, а другого - в километрах в час. Ваши суждения становятся ненадежными, потому что измерения проводятся в разных единицах измерения. Аналогичным образом, прогнозы модели выражаются в системе координат, отличной от той, в которой находится истинное целевое значение. Иными словами, модели разрешено создавать собственные единицы "на лету".
Иногда такие несовпадения единиц измерения безвредны, в тех случаях, когда целевое значение действительно находится в пределах диапазона входных параметров, эта ошибка смещения может быть почти равна 0. Но в трейдинге игнорирование их может дорого обойтись. Модели машинного обучения выполняют преобразования координат за кулисами, помещая нас в систему координат, отличную от целевой. На финансовых рынках, в отличие от естественных наук, нам не гарантируется, что наши входные параметры полностью объясняют цель. Здесь мы работаем частично вслепую.
В нашей серии статей, посвященных самооптимизирующимся советникам, мы обсудили, как можно построить модели линейной регрессии с использованием матричной факторизации, представили библиотеку OpenBLAS и объяснили метод сингулярного разложения (SVD). Читателям, незнакомым с этим обсуждением, следует ознакомиться с ним, поскольку наша статья основана на этом фундаменте, ссылка на который приведена здесь.
Для знакомых с материалом читателей напомним, что SVD преобразует матрицу в три меньшие матрицы: U, S и VT. Каждая из них обладает особыми геометрическими свойствами. U и VT являются ортогональными матрицами, что означает, что они представляют собой периодические повороты или отражения исходных данных — и, что особенно важно, они не растягивают векторы, а только меняют направление. S, средняя матрица, является диагональной и масштабирует значения данных.
В совокупности SVD можно понимать как последовательность поворота, масштабирования и поворота, применяемых к данным. Именно так модели линейной регрессии встраивают изображения целевого объекта в пространство входных параметров. Следовательно, если мы сведем линейную регрессию к ее геометрической сущности, то увидим, что она просто поворачивает, масштабируется и снова поворачивает. Ничего больше. Только это. Поворот, масштабирование, поворот. Изучение геометрии научит вас видеть ее таким образом, но как только вы это сделаете, возникнет провокационный вопрос: где на самом деле происходит все это “обучение”?
Ответ вызывает тревогу. То, что практикующие специалисты называют “обучением”, на самом деле является ничем иным, как выравниванием систем координат и изменением масштаба осей таким образом, чтобы цель могла быть спроецирована на диапазон входных параметров. Мы не раскрываем скрытую правду в данных. Мы применяем последовательность геометрических преобразований до тех пор, пока два многообразия не выстроятся в линию ровно настолько, чтобы предсказания выглядели обоснованными.
По сути, SVD - это процесс, с помощью которого создается новая система координат. При линейной регрессии входные данные проецируются на набор ортогональных осей, масштабируются и поворачиваются назад, создавая преобразованное пространство, при котором можно максимально приблизиться к целевому значению. “Обучение” модели на самом деле заключается всего лишь в приведении цели в соответствие с этой новой системой координат.
Исходя из этой геометрической структуры, мы можем обосновать действия и лучшие практики, специфичные для данной области, которые в противном случае показались бы необоснованными. Главный вывод заключается в том, что мы должны прекратить прямое сравнение прогнозов модели с реальным значением целевого показателя. Вместо этого мы должны сравнивать прогнозы модели друг с другом на разных горизонтах.
Например, предположим, что модель предсказывает, что цена закрытия на один шаг вперед составит 5 долларов, а на десять шагов вперед - 15 долларов. Разница между этими прогнозами положительная, поэтому мы покупаем. Если наклон отрицательный, мы продаем. Мы перестаем ожидать, что прогнозы будут полностью соответствовать реальности, потому что несоответствия многообразия могут навсегда сделать это невозможным — и вместо этого торгуем, исходя из относительной разницы прогнозов. Этот формат многоступенчатого прогнозирования не является чем-то новым для алгоритмической торговли, но, скорее, цель этой статьи - донести до читателя, что многоступенчатые прогнозы должны быть де-факто золотым стандартом при использовании моделей машинного обучения.
Эта статья не претендует на уменьшение или устранение геометрической погрешности. Вместо этого она учит, как свести к минимуму наше взаимодействие с этим, оставаясь за пределами области, где доминирует ошибка.
В нашей методологии мы начали с модели машинного обучения, обученной на девяти различных целевых значениях. Эти целевые значения состояли из скользящей средней цен закрытия, максимума и минимума на 1, 5 и 10 дневных свечах в будущем. Настройка управления проводилась в соответствии с классическим подходом: спрогнозировать действительное значение целевой свечи 1 в будущем, сравнитть его с текущим значением цели и торговать соответствующим образом. Как увидит читатель, мы неоднократно превосходили эту классическую методологию, отказываясь от прямых сравнений и вместо этого сравнивая собственные прогнозы модели на нескольких горизонтах. Общая идея проста, но действенна: прогнозы нашей модели могут оказаться для нас более выгодными, если сравнивать их с самими собой, чем с реальным целевым значением.
Мы протестировали контрольные настройки на исторических данных за 3 года, охватывающих период с марта 2022 по май 2025 года. За этот период контрольная настройка принесла чистую прибыль в размере 71 доллара. Всего лишь изменив интерпретацию прогнозов модели, мы увеличили чистую прибыль до 180 долларов. Прибыльность увеличилась на 153%. Наш коэффициент Шарпа вырос с 0,45 до 2,16, а процент прибыльных сделок вырос с 46% до 65%, что означает повышение точности торговли на 41%.
Ключевым выводом является то, что все улучшения, которые мы сейчас продемонстрируем читателю, будут выполнены без изменения модели, от которой мы зависим, и могут быть применены к любой другой модели машинного обучения, которая уже известна читателю.
Получение рыночных данных
Начнём с написания скрипта на MQL5 для получения необходимых нам исторических рыночных данных. Извлечение исторических рыночных данных из вашего терминала MetaTrader 5 является наилучшей практикой, поскольку гарантирует, что наши модели ONNX будут обучены на исторических данных, соответствующих конечной рабочей среде. Наш скрипт в MQL5 извлекает подробные записи о четырех доминирующих ценовых уровнях и их скользящих средних. Мы также обращаем внимание на рост каждого из этих ценовых уровней по сравнению с их предыдущими уровнями на 5 шагов в прошлом. Все эти данные записываются в формат CSV и сохраняются на вашем жестком диске.
//+------------------------------------------------------------------+ //| ProjectName | //| Copyright 2020, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs //--- Define our moving average indicator #define MA_PERIOD 5 //--- Moving Average Period #define MA_TYPE MODE_SMA //--- Type of moving average we have #define HORIZON 5 //--- Forecast horizon //--- Our handlers for our indicators int ma_handle,ma_o_handle,ma_h_handle,ma_l_handle; //--- Data structures to store the readings from our indicators double ma_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[]; //--- File name string file_name = Symbol() + " Detailed Market Data As Series Moving Average.csv"; //--- Amount of data requested input int size = 3000; //+------------------------------------------------------------------+ //| Our script execution | //+------------------------------------------------------------------+ void OnStart() { int fetch = size + (HORIZON * 2); //---Setup our technical indicators ma_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE); ma_o_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_OPEN); ma_h_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_HIGH); ma_l_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_LOW); //---Set the values as series CopyBuffer(ma_handle,0,0,fetch,ma_reading); ArraySetAsSeries(ma_reading,true); CopyBuffer(ma_o_handle,0,0,fetch,ma_o_reading); ArraySetAsSeries(ma_o_reading,true); CopyBuffer(ma_h_handle,0,0,fetch,ma_h_reading); ArraySetAsSeries(ma_h_reading,true); CopyBuffer(ma_l_handle,0,0,fetch,ma_l_reading); ArraySetAsSeries(ma_l_reading,true); //---Write to file int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(int i=size;i>=1;i--) { if(i == size) { FileWrite(file_handle,"Time", //--- OHLC "True Open", "True High", "True Low", "True Close", //--- MA OHLC "True MA C", "True MA O", "True MA H", "True MA L", //--- Growth in OHLC "Diff Open", "Diff High", "Diff Low", "Diff Close", //--- Growth in MA OHLC "Diff MA Close 2", "Diff MA Open 2", "Diff MA High 2", "Diff MA Low 2" ); } else { FileWrite(file_handle, iTime(_Symbol,PERIOD_CURRENT,i), //--- OHLC iClose(_Symbol,PERIOD_CURRENT,i), iOpen(_Symbol,PERIOD_CURRENT,i), iHigh(_Symbol,PERIOD_CURRENT,i), iLow(_Symbol,PERIOD_CURRENT,i), //--- MA OHLC ma_reading[i], ma_o_reading[i], ma_h_reading[i], ma_l_reading[i], //--- Growth in OHLC iOpen(_Symbol,PERIOD_CURRENT,i) - iOpen(_Symbol,PERIOD_CURRENT,(i + HORIZON)), iHigh(_Symbol,PERIOD_CURRENT,i) - iHigh(_Symbol,PERIOD_CURRENT,(i + HORIZON)), iLow(_Symbol,PERIOD_CURRENT,i) - iLow(_Symbol,PERIOD_CURRENT,(i + HORIZON)), iClose(_Symbol,PERIOD_CURRENT,i) - iClose(_Symbol,PERIOD_CURRENT,(i + HORIZON)), //--- Growth in MA OHLC ma_reading[i] - ma_reading[(i + HORIZON)], ma_o_reading[i] - ma_o_reading[(i + HORIZON)], ma_h_reading[i] - ma_h_reading[(i + HORIZON)], ma_l_reading[i] - ma_l_reading[(i + HORIZON)] ); } } //--- Close the file FileClose(file_handle); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef HORIZON #undef MA_PERIOD #undef MA_TYPE //+------------------------------------------------------------------+
Анализ рыночных данных
Теперь мы можем подготовиться к ознакомлению с нашими историческими рыночными данными. Во-первых, загрузим несколько библиотек python для работы с данными.
import pandas as pd import numpy as np import matplotlib.pyplot as plt
Теперь определим три уникальных временных горизонта, которые нам надо спрогнозировать.
#Define our different forecast horizons H1 = 1 H2 = 5 H3 = 10
Теперь будем считывать рыночные данные, которые экспортировали из нашего терминала, и создавать целевые значения для скользящих средних на разных временных горизонтах.
#Read in the data data = pd.read_csv('../EURUSD Detailed Market Data As Series Moving Average.csv') #Label the data data['Target 1'] = data['True MA C'].shift(-H1) data['Target 2'] = data['True MA C'].shift(-H2) data['Target 3'] = data['True MA C'].shift(-H3) data['Target 4'] = data['True MA H'].shift(-H1) data['Target 5'] = data['True MA H'].shift(-H2) data['Target 6'] = data['True MA H'].shift(-H3) data['Target 7'] = data['True MA L'].shift(-H1) data['Target 8'] = data['True MA L'].shift(-H2) data['Target 9'] = data['True MA L'].shift(-H3) #Drop missing rows data = data.iloc[:-H3,:]
Создадим обучающий раздел.
data = data.iloc[:-(365*3),:] data_test = data.iloc[-(365*3):,:]
Разделим входные параметры и целевые показатели.
X = data.iloc[:,1:-9] y = data.iloc[:,-9:]
Загрузите любую модель машинного обучения по вашему выбору. Для своего обсуждения будем использовать библиотеку sklearn и продемонстрируем основные принципы с помощью линейной модели.
from sklearn.linear_model import LinearRegression
Инициализируем модель.
model = LinearRegression()
Обучим модель.
model.fit(X,y)
Получаем предсказания модели на тестовом наборе, но не обучайте модель на тестовом наборе. Как мы видим, прогнозы модели, по-видимому, хорошо согласуются с целевым значением, но, как мы сейчас увидим, наша модель может демонстрировать даже лучшие результаты, чем этот.
preds = pd.DataFrame(model.predict(data_test.iloc[:,1:-9])) plt.plot(data_test.iloc[:,-9].reset_index(drop=True),color='black') plt.plot(preds.iloc[:,0],color='red',linestyle=':') plt.grid() plt.title("Out Of Sample Forecasting") plt.ylabel('EUR/USD Exchange Rate') plt.xlabel('Time') plt.legend(['Actual Price','Forecasted Price'])

Рисунок 1: Вневыборочные прогнозы модели кажутся согласующимися с реальным целевым значением, но этот уровень результатов может быть превзойден
ONNX расшифровывается как Open Neural Network Exchange и позволяет создавать и развертывать модели машинного обучения в стандартизированной библиотеке, которая используется все большим числом языков программирования. Будем использовать библиотеку ONNX для экспорта нашей модели машинного обучения из Python, а затем импортируем ее в MQL5. ONNX позволяет быстро разрабатывать модели машинного обучения и внедрять их.
import onnx from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorType
Мы должны определить входную и выходную форму наших моделей ONNX. Это легко сделать, потому что ранее мы разделили входные и выходные параметры. Просто выберем количество столбцов в каждом разделе и сохраним их. Pandas упрощает извлечение этой информации с помощью свойства shape.
initial_types = [("FLOAT INPUT",FloatTensorType([1,X.shape[1]]))] final_types = [("FLOAT OUTPUT",FloatTensorType([y.shape[1],1]))]
Создайте ONNX-прототип модели машинного обучения. Укажем количество входных параметров и выходных данных, которые нам нужны для нашей модели.
model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12) Сохраним модель ONNX на диск.
onnx.save(model_proto,"EURUSD MFH LR D1.onnx")
Установление базового уровня показателей эффективности
Теперь мы готовы к установлению базового уровня показателей эффективности. Начинаем с определения как можно большего количества параметров стратегии, чтобы обеспечить согласованность тестов. //+------------------------------------------------------------------+ //| MFH.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/ru/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ru/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System definitions | //+------------------------------------------------------------------+ #define SYSTEM_INPUTS 16 #define SYSTEM_OUTPUTS 9 #define ATR_PERIOD 14 #define ATR_PADDING 1 #define TF_1 PERIOD_D1 #define TF_2 PERIOD_M15 #define MA_PERIOD 5 #define MA_TYPE MODE_SMA #define HORIZON 5
Загрузим буфер ONNX, который мы экспортировали из Python.
//+------------------------------------------------------------------+ //| System resources | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD MFH LR D1.onnx" as const uchar onnx_buffer[];
Мы также будем использовать несколько библиотек для решения рутинных задач нашей алгоритмической торговли, таких как исполнение сделок, формирование свечей и обработка буфера ONNX.
//+------------------------------------------------------------------+ //| System libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> #include <VolatilityDoctor\ONNX\ONNXFloat.mqh> #include <VolatilityDoctor\Time\Time.mqh> #include <VolatilityDoctor\Trade\TradeInfo.mqh> ONNXFloat *ONNXHandler; Time *TimeHandler; Time *LowerTimeHandler; TradeInfo *TradeHandler; CTrade Trade;
Необходимо несколько глобальных переменных, в основном для работы с техническими индикаторами и хранения прогнозов модели ONNX.
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ma_handle,ma_o_handle,ma_h_handle,ma_l_handle,fetch,atr_handler; double ma_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[],atr[]; double padding; vector model_prediction;
Когда наше приложение будет загружено в первый раз, мы инициализируем свои глобальные переменные и пользовательские классы, а также сохраним обработчики для созданных нами технических индикаторов.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- fetch = HORIZON * 2; ONNXHandler = new ONNXFloat(onnx_buffer); LowerTimeHandler = new Time(Symbol(),TF_2); TimeHandler = new Time(Symbol(),TF_1); TradeHandler = new TradeInfo(Symbol(),TF_1); ONNXHandler.DefineOnnxInputShape(0,1,SYSTEM_INPUTS); ONNXHandler.DefineOnnxOutputShape(0,1,SYSTEM_OUTPUTS); ma_handle = iMA(Symbol(),TF_1,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE); ma_o_handle = iMA(Symbol(),TF_1,MA_PERIOD,0,MA_TYPE,PRICE_OPEN); ma_h_handle = iMA(Symbol(),TF_1,MA_PERIOD,0,MA_TYPE,PRICE_HIGH); ma_l_handle = iMA(Symbol(),TF_1,MA_PERIOD,0,MA_TYPE,PRICE_LOW); atr_handler = iATR(Symbol(),TF_1,MA_PERIOD); model_prediction = vector::Zeros(SYSTEM_OUTPUTS); //--- return(INIT_SUCCEEDED); }
В MQL5 принято эффективно управлять памятью и освобождать ресурсы, которые больше не используются.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- delete ONNXHandler; IndicatorRelease(ma_h_handle); IndicatorRelease(ma_o_handle); IndicatorRelease(ma_l_handle); IndicatorRelease(ma_handle); IndicatorRelease(atr_handler); }
Когда будут получены новые ценовые уровни, мы обновим свои буферы технических индикаторов и уровни стоп-лосса. После этого проверим, есть ли у нас открытые позиции. Если ни одна из них не открыта, проверим наличие торговой возможности. В противном случае будем управлять открытой позицией с помощью трейлинг-стоп-лосса.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Has a new candle formed if(TimeHandler.NewCandle()) { //---Set the values as series CopyBuffer(ma_handle,0,0,fetch,ma_reading); ArraySetAsSeries(ma_reading,true); CopyBuffer(ma_o_handle,0,0,fetch,ma_o_reading); ArraySetAsSeries(ma_o_reading,true); CopyBuffer(ma_h_handle,0,0,fetch,ma_h_reading); ArraySetAsSeries(ma_h_reading,true); CopyBuffer(ma_l_handle,0,0,fetch,ma_l_reading); ArraySetAsSeries(ma_l_reading,true); CopyBuffer(atr_handler,0,0,fetch,atr); ArraySetAsSeries(atr,true); padding = (atr[0] * ATR_PADDING); //--- Obtain a prediction from our model if(PositionsTotal() == 0) { model_predict(); } //--- Manage open positions if(PositionsTotal() >0) { manage_setup(); } } } //+------------------------------------------------------------------+
Наш трейлинг-стоп-лосс будет определяться индикатором ATR (Средний истинный диапазон). ATR измеряет волатильность рынка и помогает динамично корректировать уровни риска. Если стоп-лосс можно безопасно обновить до более прибыльной позиции, мы это сделаем, в противном случае будем ждать.
//+------------------------------------------------------------------+ //| Manage our open positions | //+------------------------------------------------------------------+ void manage_setup(void) { //--- Select the position by its ticket number if(PositionSelectByTicket(PositionGetTicket(0))) { //--- Store the current tp and sl levels double current_tp,current_sl; current_tp = PositionGetDouble(POSITION_TP); current_sl = PositionGetDouble(POSITION_SL); //--- Before we calculate the new stop loss or take profit double new_sl,new_tp; //--- We first check the position type if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { new_sl = TradeHandler.GetBid()-padding; new_tp = TradeHandler.GetBid()+padding; //--- Check if the new stops are more profitable if(new_sl>current_sl) Trade.PositionModify(Symbol(),new_sl,new_tp); } if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { new_sl = TradeHandler.GetAsk()+padding; new_tp = TradeHandler.GetAsk()-padding; //--- Check if the new stops are more profitable if(new_sl<current_sl) Trade.PositionModify(Symbol(),new_sl,new_tp); } } }
Сначала протестируем наше приложение без модели машинного обучения, чтобы определить базовый уровень прибыльности. Будем использовать простую стратегию пробоя, чтобы наши модели машинного обучения были более эффективными. Модели, не соответствующие этому уровню результатов, неприемлемы.
//+------------------------------------------------------------------+ //| Obtain a prediction from our model | //+------------------------------------------------------------------+ void model_predict(void) { if(iHigh(Symbol(),TF_2,1)<iOpen(Symbol(),TF_2,0)) { Trade.Buy(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); } if(iLow(Symbol(),TF_2,1)>iOpen(Symbol(),TF_2,0)) { Trade.Sell(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); } } //+------------------------------------------------------------------+
Наконец, отменим определение всех системных констант, созданных нами ранее.
//+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef MA_PERIOD #undef MA_TYPE #undef HORIZON #undef TF_1 #undef TF_2 #undef SYSTEM_INPUTS #undef SYSTEM_OUTPUTS #undef ATR_PADDING #undef ATR_PERIOD
В целом, наша система выглядит именно так.
//+------------------------------------------------------------------+ //| MFH.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/ru/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ru/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System definitions | //+------------------------------------------------------------------+ #define SYSTEM_INPUTS 16 #define SYSTEM_OUTPUTS 9 #define ATR_PERIOD 14 #define ATR_PADDING 1 #define TF_1 PERIOD_D1 #define TF_2 PERIOD_M15 #define MA_PERIOD 5 #define MA_TYPE MODE_SMA #define HORIZON 5 //+------------------------------------------------------------------+ //| System resources | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD MFH LR D1.onnx" as const uchar onnx_buffer[]; //+------------------------------------------------------------------+ //| System libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> #include <VolatilityDoctor\ONNX\ONNXFloat.mqh> #include <VolatilityDoctor\Time\Time.mqh> #include <VolatilityDoctor\Trade\TradeInfo.mqh> ONNXFloat *ONNXHandler; Time *TimeHandler; Time *LowerTimeHandler; TradeInfo *TradeHandler; CTrade Trade; //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ma_handle,ma_o_handle,ma_h_handle,ma_l_handle,fetch,atr_handler; double ma_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[],atr[]; double padding; vector model_prediction; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- fetch = HORIZON * 2; ONNXHandler = new ONNXFloat(onnx_buffer); LowerTimeHandler = new Time(Symbol(),TF_2); TimeHandler = new Time(Symbol(),TF_1); TradeHandler = new TradeInfo(Symbol(),TF_1); ONNXHandler.DefineOnnxInputShape(0,1,SYSTEM_INPUTS); ONNXHandler.DefineOnnxOutputShape(0,1,SYSTEM_OUTPUTS); ma_handle = iMA(Symbol(),TF_1,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE); ma_o_handle = iMA(Symbol(),TF_1,MA_PERIOD,0,MA_TYPE,PRICE_OPEN); ma_h_handle = iMA(Symbol(),TF_1,MA_PERIOD,0,MA_TYPE,PRICE_HIGH); ma_l_handle = iMA(Symbol(),TF_1,MA_PERIOD,0,MA_TYPE,PRICE_LOW); atr_handler = iATR(Symbol(),TF_1,MA_PERIOD); model_prediction = vector::Zeros(SYSTEM_OUTPUTS); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- delete ONNXHandler; IndicatorRelease(ma_h_handle); IndicatorRelease(ma_o_handle); IndicatorRelease(ma_l_handle); IndicatorRelease(ma_handle); IndicatorRelease(atr_handler); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Has a new candle formed if(TimeHandler.NewCandle()) { //---Set the values as series CopyBuffer(ma_handle,0,0,fetch,ma_reading); ArraySetAsSeries(ma_reading,true); CopyBuffer(ma_o_handle,0,0,fetch,ma_o_reading); ArraySetAsSeries(ma_o_reading,true); CopyBuffer(ma_h_handle,0,0,fetch,ma_h_reading); ArraySetAsSeries(ma_h_reading,true); CopyBuffer(ma_l_handle,0,0,fetch,ma_l_reading); ArraySetAsSeries(ma_l_reading,true); CopyBuffer(atr_handler,0,0,fetch,atr); ArraySetAsSeries(atr,true); padding = (atr[0] * ATR_PADDING); } if(LowerTimeHandler.NewCandle()) { //--- Obtain a prediction from our model if(PositionsTotal() == 0) { model_predict(); } //--- Manage open positions if(PositionsTotal() >0) { manage_setup(); } } } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Manage our open positions | //+------------------------------------------------------------------+ void manage_setup(void) { //--- Select the position by its ticket number if(PositionSelectByTicket(PositionGetTicket(0))) { //--- Store the current tp and sl levels double current_tp,current_sl; current_tp = PositionGetDouble(POSITION_TP); current_sl = PositionGetDouble(POSITION_SL); //--- Before we calculate the new stop loss or take profit double new_sl,new_tp; //--- We first check the position type if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { new_sl = TradeHandler.GetBid()-padding; new_tp = TradeHandler.GetBid()+padding; //--- Check if the new stops are more profitable if(new_sl>current_sl) Trade.PositionModify(Symbol(),new_sl,new_tp); } if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { new_sl = TradeHandler.GetAsk()+padding; new_tp = TradeHandler.GetAsk()-padding; //--- Check if the new stops are more profitable if(new_sl<current_sl) Trade.PositionModify(Symbol(),new_sl,new_tp); } } } //+------------------------------------------------------------------+ //| Obtain a prediction from our model | //+------------------------------------------------------------------+ void model_predict(void) { if(iHigh(Symbol(),TF_2,1)<iOpen(Symbol(),TF_2,0)) { Trade.Buy(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); } if(iLow(Symbol(),TF_2,1)>iOpen(Symbol(),TF_2,0)) { Trade.Sell(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); } } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef MA_PERIOD #undef MA_TYPE #undef HORIZON #undef TF_1 #undef TF_2 #undef SYSTEM_INPUTS #undef SYSTEM_OUTPUTS #undef ATR_PADDING #undef ATR_PERIOD //+------------------------------------------------------------------+
Начинаем с того, что сначала устанавливаем разумные ожидания в отношении прибыльности. Выполнение этого шага имеет существенное значение для того, чтобы мы могли объективно оценивать те улучшения, которые действительно вносят наши модели машинного обучения.

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

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

Рисунок 4: Оригинальная версия нашей торговой стратегии не вызывает особого доверия ни у одного разработчика
Классическая попытка превзойти контроль
Теперь попытаемся превзойти контрольную торговую стратегию, используя классическую настройку трейдинга. Обычно, при классической настройке, мы прогнозируем цель на 1 свечу в будущем, а затем сравниваем прогнозируемую цену с действительным значением цены, чтобы получить наши торговые сигналы. В этой статье мы пытаемся убедить читателя в том, что такая практика, возможно, не самая лучшая из возможных для нашего сообщества, давайте разберемся, почему.
Большая часть кода приложения не будет изменена намеренно, поэтому мы можем сосредоточиться исключительно на той части кода на MQL5, которая должна быть изменена для проверки наших идей. Как читатель может видеть ниже, теперь мы должны получить 16 входных параметров, необходимых нашей модели ONNX для составления прогнозов, и обязательно преобразовать каждый из них в типы данных float, прежде чем приступать к каким-либо вычислениям. После этого мы получим прогноз из нашей модели ONNX и сравним его с действительным значением целевого показателя.
//+------------------------------------------------------------------+ //| Obtain a prediction from our model | //+------------------------------------------------------------------+ void model_predict(void) { vectorf model_inputs(SYSTEM_INPUTS); model_inputs[0] = (float) iClose(_Symbol,PERIOD_CURRENT,0); model_inputs[1] = (float) iOpen(_Symbol,PERIOD_CURRENT,0); model_inputs[2] = (float) iHigh(_Symbol,PERIOD_CURRENT,0); model_inputs[3] = (float) iLow(_Symbol,PERIOD_CURRENT,0); model_inputs[4] = (float) ma_reading[0]; model_inputs[5] = (float) ma_o_reading[0]; model_inputs[6] = (float) ma_h_reading[0]; model_inputs[7] = (float) ma_l_reading[0]; model_inputs[8] = (float)(iOpen(_Symbol,PERIOD_CURRENT,0) - iOpen(_Symbol,PERIOD_CURRENT,(0 + HORIZON))); model_inputs[9] = (float)(iHigh(_Symbol,PERIOD_CURRENT,0) - iHigh(_Symbol,PERIOD_CURRENT,(0 + HORIZON))); model_inputs[10] = (float)(iLow(_Symbol,PERIOD_CURRENT,0) - iLow(_Symbol,PERIOD_CURRENT,(0 + HORIZON))); model_inputs[11] = (float)(iClose(_Symbol,PERIOD_CURRENT,0) - iClose(_Symbol,PERIOD_CURRENT,(0 + HORIZON))); model_inputs[12] = (float)(ma_reading[0] - ma_reading[(0 + HORIZON)]); model_inputs[13] = (float)(ma_o_reading[0] - ma_o_reading[(0 + HORIZON)]); model_inputs[14] = (float)(ma_h_reading[0] - ma_h_reading[(0 + HORIZON)]); model_inputs[15] = (float)(ma_l_reading[0] - ma_l_reading[(0 + HORIZON)]); //--- Obtain the prediction ONNXHandler.Predict(model_inputs); for(int i=0;i<SYSTEM_OUTPUTS;i++) { model_prediction[i] = ONNXHandler.GetPrediction(i); } if(iHigh(Symbol(),TF_2,1)<iOpen(Symbol(),TF_2,0)) { if(model_prediction[0]>iClose(Symbol(),TF_2,0)) Trade.Buy(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); } if(iLow(Symbol(),TF_2,1)>iOpen(Symbol(),TF_2,0)) { if(model_prediction[0]<iClose(Symbol(),TF_2,0) Trade.Sell(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); } } //+------------------------------------------------------------------+
Используем классический торговый алгоритм машинного обучения в течение того же периода тестирования, который мы использовали для определения контрольных уровней прибыльности.

Рисунок 5: Выполнение первой попытки превзойти контрольные настройки
Уровни прибыльности нашего торгового приложения значительно снизились. Несмотря на то, что стратегия продемонстрировала высокую точность и 63% всех сделок, совершенных по ней, указаны как прибыльные, это вряд ли впечатляет, поскольку она не смогла превысить уровень прибыли в 71 доллар, установленный контрольным приложением.

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

Рисунок 7: Кривая эквити, полученная с помощью улучшенной версии нашего торгового приложения, менее волатильна, чем в оригинальной стратегии, но и она не достигает тех же высот
Доведение классической попытки до пределов
Напомним, что мы прогнозируем достижение целевого значения на 1, 5 и 10 свечах в будущем. Давайте посмотрим, может ли прогноз на 10 свечей быть более информативным, чем простой прогноз на 1 шаг, с которого мы начали. Поэтому, как и раньше, мы сосредоточимся только на тех частях торгового приложения, которые необходимо было изменить, чтобы провести справедливое сравнение.
if(iHigh(Symbol(),TF_2,1)<iOpen(Symbol(),TF_2,0)) { if(model_prediction[ 2 ]>iClose(Symbol(),TF_2,0)) Trade.Buy(TradeHandler.MinVolume()*2,TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); Trade.Buy(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); } if(iLow(Symbol(),TF_2,1)>iOpen(Symbol(),TF_2,0)) { if(model_prediction[ 2 ]<iClose(Symbol(),TF_2,0)) Trade.Sell(TradeHandler.MinVolume()*2,TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); Trade.Sell(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); }
Как и в случае с контрольной настройкой торгового приложения, мы выберем тот же 3-летний период для тестирования нашего приложения.

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

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

Рисунок 10: Кривая эквити, полученная в результате второго цикла нашего торгового приложения, превосходит контрольную настройку, но сейчас мы продемонстрируем читателю, как достичь новых высот
Новые возможности для улучшения
Теперь мы готовы сравнить прогнозы нашей модели на несколько шагов в будущее. Мы сравним, где модель ожидает, что высокая цена будет на 1 и 10 шагах от текущего момента, а затем используем ожидаемый рост этого ценового уровня в качестве нашего торгового сигнала. if(iHigh(Symbol(),TF_2,1)<iOpen(Symbol(),TF_2,0)) { if(model_prediction[3]<model_prediction[5]) Trade.Buy(TradeHandler.MinVolume()*2,TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); Trade.Buy(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); } if(iLow(Symbol(),TF_2,1)>iOpen(Symbol(),TF_2,0)) { if(model_prediction[3]>model_prediction[5]) Trade.Sell(TradeHandler.MinVolume()*2,TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); Trade.Sell(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); }
Проанализируем, есть ли какой-либо смысл в сравнении прогнозов модели на нескольких временных горизонтах по сравнению с более простыми прямыми сравнениями между прогнозами модели и действительным значением целевого показателя.

Рисунок 11: Запуск третьей версии нашего торгового приложения в течение трехлетнего периода тестирования на истории
Как видит читатель, полученные нами результаты говорят сами за себя. Наше приложение сейчас более прибыльно, чем когда-либо на любом предыдущем этапе нашего цикла разработки. Напомним, что используем ту же модель ONNX, которую экспортировали ранее. И это при том, что суть наших торговых условий не изменилась. Скорее, тщательно интерпретируя прогнозы нашей модели, мы, по-видимому, извлекаем больше пользы из той же торговой стратегии.

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

Рисунок 13: Кривая эквити, полученная в текущем цикле нашего торгового приложения, достигает новых максимумов, которых мы не могли достичь во всех предыдущих версиях приложения.
Финальные улучшения
Как автору, мне нравится искать любую структуру рынка, которую легче предсказать, чем саму цену, но которая не менее информативна, чем знание самих будущих уровней цен. Поэтому, учитывая, что мы имеем дело со скользящими средними в канале высоких и низких цен, моя интуиция заставила меня задаться вопросом, не проще ли спрогнозировать рост в средней точке между этими двумя скользящими средними. Это определенно стало причиной данного обсуждения.
if(iHigh(Symbol(),TF_2,1)<iOpen(Symbol(),TF_2,0)) { if(((model_prediction[3]+model_prediction[6])/2)<((model_prediction[5]+model_prediction[8])/2)) Trade.Buy(TradeHandler.MinVolume()*2,TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); Trade.Buy(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetAsk(),TradeHandler.GetBid()-padding,TradeHandler.GetBid()+padding); } if(iLow(Symbol(),TF_2,1)>iOpen(Symbol(),TF_2,0)) { if(((model_prediction[3]+model_prediction[6])/2)>((model_prediction[5]+model_prediction[8])/2)) Trade.Sell(TradeHandler.MinVolume()*2,TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); Trade.Sell(TradeHandler.MinVolume(),TradeHandler.GetSymbol(),TradeHandler.GetBid(),TradeHandler.GetAsk()+padding,TradeHandler.GetAsk()-padding); }
Запустим окончательную версию нашего приложения на том же 3-летнем периоде, с которым мы работали.

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

Рисунок 15: Финальные подробные уровни эффективности нашего приложения превосходят все другие уровни эффективности, установленные нами ранее в ходе нашего разговора
Волатильность, которую мы наблюдали на нашей кривой эквити, почти полностью находится под нашим контролем. Удивительно, как много прибыли можно получить, не добавляя каких-либо новых сложностей в ваши торговые стратегии машинного обучения.

Рисунок 16: Визуализация кривой эквити, полученной в финальной версии нашего торгового приложения, дает нам уверенность во всех внесенных нами на данный момент изменениях
Заключение
Идея прогнозирования на несколько временных отрезков вперед не нова для сообщества алгоритмической торговли. Что является новым — и на чем настаивает эта статья — так это на том, что прогнозирование на несколько временных отрезков следует рассматривать не как альтернативный метод, а скорее как потенциальный золотой стандарт для самой алгоритмической торговли.
Ранее я задавал вопрос тебе, читатель. Я продемонстрировал, что с геометрической точки зрения линейная регрессия сводится не более чем к последовательности поворотов и масштабирования. Тогда я спросил: где на самом деле происходит обучение?
Для читателей, которые все еще помнят этот вопрос, я должен предложить одну возможную гипотезу. Однако, я бы предпочел, чтобы вы независимо исследовали собственную гипотезу.
Принцип, демонстрируемый на протяжении всей этой статьи, заключается в том, что математические понятия всегда имеют геометрические аналоги. Любая модель машинного обучения, которую вы можете себе представить, может быть переосмыслена как скоординированная акробатика масштабирования, отображения, проекции, свертки и поворота, применяемая к многообразиям, определяемым самими данными. Даже продвинутые нейронные сети не являются чем-то таинственным: это просто тщательно продуманная хореография геометрических преобразований, которая снова и снова сворачивает и переформировывает данные.
Следовательно, ответ на вопрос “Где происходит обучение?” может быть таким: обучение происходит, когда информация кодируется в геометрические паттерны. Цикл поворотов и масштабирования - один из самых элементарных из этих паттернов. Это мощная геометрическая структура в том же смысле, в каком три основных цвета являются основой, из которой состоят все остальные цвета. В геометрии существуют первичные преобразования, из которых состоят все остальные.
Сообщество, занимающееся классификацией изображений, уже осознало эту реальность. Их успех обусловлен длинными и детализированными процессами предварительной обработки — процессами, которые, по сути, представляют собой тщательную координацию геометрических преобразований. Они могут показаться рутинной разработкой функциональных возможностей, но на самом деле являются тихим применением этих самых принципов, часто без полного признания их более глубокого значения.
Итак, читатель получает ценную информацию: прогнозирование на несколько временных отрезков в трейдинге, возможно, является одной из самых недооцененных стратегий в нашей области именно потому, что оно выполняет гораздо больше работы, чем на самом деле считается. Прогнозы на несколько временных отрезков гарантируют, что мы будем проводить наши сравнения в одной и той же системе координат. В противном случае прямое сравнение прогнозов вашей модели с действительным значением целевого объекта идентично сравнению двух величин, которые не гарантированно находятся в одних и тех же единицах измерения.
| Название файла | Описание файла |
|---|---|
| Fetch_Data_MA_2.1.mq5 | Скрипт на MQL5, который мы использовали для извлечения исторических данных из нашего терминала MetaTrader 5. |
| MFH_Baseline.mq5 | Базовая версия нашей торговой стратегии, в которой не использовались модели машинного обучения. |
| MFH_1.1.mq5 | Первая попытка, предпринятая нами для создания торгового приложения только в соответствии с классическими парадигмами финансового машинного обучения. |
| MFH_1.2.mq5 | Финальная попытка, предпринятая нами для создания торгового приложения с использованием классических парадигм финансового машинного обучения. |
| MFH_1.3.mq5 | Первая попытка, которую мы предприняли, чтобы отказаться от прямых сравнений между текущей ценой и прогнозируемой ценой. |
| MFH_1.4.mq5 | Окончательная версия нашей торговой стратегии, показавшая самый высокий уровень прибыльности, которую мы продемонстрировали в этой статье. |
| Multiple_Forecast_Horizons.ipynb | Jupyter Notebook, использованный для экспорта нашей модели ONNX. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/19383
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Моделирование рынка (Часть 21): Первые шаги на SQL (IV)
Знакомство с языком MQL5 (Часть 36): Освоение API и функции WebRequest в языке MQL5 (X)
Знакомство с языком MQL5 (Часть 37): Освоение API и функции WebRequest в языке MQL5 (XI)
Создание самооптимизирующихся советников на MQL5 (Часть 8): Анализ нескольких стратегий
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования