Создание самооптимизирующихся советников на MQL5 (Часть 8): Анализ нескольких стратегий (3) — Политика взвешенного голосования
Теперь мы добавим финальный компонент в наш советник с несколькими стратегиями: стратегию разворота на основе индикатора Williams Percent Range (WPR). Как и в предыдущих статьях, мы сначала жестко закодировали ручную реализацию стратегии, чтобы сравнить её эффективность с классом, который мы собираемся создать. Чтобы не утомлять читателей, мы опустим результаты тестов, использованные для проверки класса. На данный момент достаточно сказать, что читатели могут быть уверены: было проведено адекватное тестирование для подтверждения корректности и надежности представленного сегодня класса.
При создании ансамбля закономерно возникает вопрос: как доказать, что все выбранные стратегии необходимы? Можем ли мы быть уверены, что результаты не были бы лучше, если бы мы использовали только некоторые из них? Как нам убедиться в этом на собственном опыте?
К счастью, генетический оптимизатор может помочь ответить на такие сложные вопросы, если мы правильно сформулируем для него задачу.
Чтобы добиться этого, мы позволим нашим стратегиям взаимодействовать по принципу демократического голосования, где каждой из них разрешен только один голос. При этом вес голоса каждой стратегии станет настраиваемым параметром, который будет корректироваться генетическим оптимизатором. Если оптимизатор определит, что одна из наших стратегий не вносит положительного вклада в общую эффективность, он установит вес её голоса близким к нулю. И наоборот, он увеличит веса тех стратегий, которые приносят прибыль.
Таким образом, мы представляем этот фреймворк как политику взвешенного голосования, в которой изначально задаем эталонный уровень производительности, распределяя веса голосов равномерно. В нашем примере мы начинаем с того, что каждой стратегии присваивается вес 0.5 по шкале от 0 до 1.
Далее мы позволяем генетическому оптимизатору изменять эти веса, чтобы максимизировать прибыльность и определить, действительно ли все три стратегии приносят пользу.
Как оказалось, эта процедура выдает множество различных конфигураций, каждая из которых показывает, как полезность стратегии может меняться в зависимости от её конкретных настроек. В каждой уникальной конфигурации веса стратегий смещаются. Так, может возникнуть ситуация, где только одна стратегия окажется полезной, в то время как в другом наборе параметров все три будут вносить положительный вклад.
Это делает вопрос "Необходимы ли все три стратегии?" по-настоящему сложным. Наши результаты показывают, что ответ напрямую зависит от того, какую именно конфигурацию приложение использовало изначально. Приступим.
Начало работы в MQL5
К концу нашего обсуждения дерево наследования для торговых стратегий можно будет визуализировать так, как показано на Рисунке 1 ниже. У нас будет 3 индивидуальных торговых стратегии:
- Стратегия импульса на основе индекса относительной силы
- Стратегия пересечения скользящих средних
- Стратегия разворота на основе процентного диапазона Вильямса
Каждая из них обладает общим функционалом, таким как возможность подавать сигнал на вход в длинную или короткую позицию. Все три наши стратегии имеют общего базового предка. Это важно для обеспечения единообразия инструментов во всех наших классах.
В сегодняшнем обсуждении мы сосредоточимся на реализации последней из трех стратегий, изображенных на Рисунке 1: стратегии разворота на основе Williams Percent Range (WPR). После этого наш генетический оптимизатор скорректирует веса, присвоенные каждой стратегии, чтобы гарантировать, что наименее прибыльная из них не ухудшит общие показатели нашего приложения.

Рисунок 1: Текущее состояние дерева наследования, общего для наших торговых стратегий
Кроме того, на Рисунке 2 мы представили иллюстрации, иллюстрирующие основную идеологию нашей стратегии. Обратите внимание, что в примере на Рисунке 2 общая сумма весов всех голосов не обязательно равна единице. Хотя наложение такого ограничения является обычной практикой, в данном случае мы решили этого не делать. Мы можем рассмотреть этот вариант в будущем, так как он потребует несколько иного алгоритма, чем тот, который реализован сегодня.
На данный момент мы просто позволяем генетическому оптимизатору присваивать каждой из трех стратегий значение в диапазоне от 0 до 1 включительно. Генетическому оптимизатору будет проще генерировать прибыльные конфигурации, если ему не придется учитывать наименее эффективную стратегию. В этом и заключается суть нашей дискуссии: мы хотим продемонстрировать, что генетический оптимизатор способен самостоятельно "подрезать" дерево торговых стратегий, одновременно настраивая другие важные параметры.

Рисунок 2: Визуализация весов, присвоенных каждой стратегии генетическим оптимизатором
Первым шагом в реализации нашей стратегии является загрузка зависимостей. Первая зависимость, как и в случае с нашей предыдущей стратегией на основе Williams Percent Range (WPR), — это загрузка класса индикатора Williams Percentage с одним буфером, который мы создали ранее. После этого мы загружаем родительский класс стратегии, который мы также разработали в рамках отдельного обсуждения.
//+------------------------------------------------------------------+ //| WPRReversal.mqh | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| Dependencies | //+------------------------------------------------------------------+ #include <VolatilityDoctor\Indicators\WPR.mqh> #include <VolatilityDoctor\Strategies\Parent\Strategy.mqh>
После загрузки зависимостей мы можем приступить к определению членов нашего класса. Первым членом будет указатель на индикатор Williams Percent Range, который мы собираемся использовать. Это будет закрытый член — единственный закрытый член нашего класса. Остальные члены класса являются открытыми. В частности, мы включаем конструктор, деструктор и виртуальные методы, унаследованные от родительского класса.
class WPRReversal : public Strategy { private: //--- The instance of the RSI used in this strategy WPR *my_wpr; public: //--- Class constructor WPRReversal(string user_symbol,ENUM_TIMEFRAMES user_timeframe,int user_period); //--- Class destructor ~WPRReversal(); //--- Class overrides virtual bool Update(void); virtual bool BuySignal(void); virtual bool SellSignal(void); };
Мы начинаем с переопределения метода update. Метод update просто вызывает метод set_indicator_values — функцию, которая присутствует во всех наших классах индикаторов. Эта функция заполняет наш буфер индикатора значениями WPR, полученными из терминала. Она выполняет предварительную проверку, чтобы убедиться, что количество полученных данных не равно нулю, прежде чем вернуть управление вызывающему контексту.
//+------------------------------------------------------------------+ //| Our strategy update method | //+------------------------------------------------------------------+ bool WPRReversal::Update(void) { //--- Set the indicator value my_wpr.SetIndicatorValues(Strategy::GetIndicatorBufferSize(),true); //--- Check readings are valid if(my_wpr.GetCurrentReading() != 0) return(true); //--- Something went wrong return(false); }
После этого мы определяем два метода, используемых для подачи сигналов на вход в покупку и продажу. Эти методы просто возвращают значение true, если соответствующие им условия выполнены.
//+------------------------------------------------------------------+ //| Check for our buy signal | //+------------------------------------------------------------------+ bool WPRReversal::BuySignal(void) { //--- Buy signals when the RSI is above 50 return(my_wpr.GetCurrentReading()>50); } //+------------------------------------------------------------------+ //| Check for our sell signal | //+------------------------------------------------------------------+ bool WPRReversal::SellSignal(void) { //--- Sell signals when the RSI is below 50 return(my_wpr.GetCurrentReading()<50); }
Наконец, мы определяем наш параметрический конструктор, который принимает символ, таймфрейм и период, необходимые для инициализации индикатора WPR. Деструктор же просто удаляет указатель, созданный нами для нового экземпляра объекта класса WPR.
//+------------------------------------------------------------------+ //| Our class constructor | //+------------------------------------------------------------------+ WPRReversal::WPRReversal(string user_symbol,ENUM_TIMEFRAMES user_timeframe,int user_period) { my_wpr = new WPR(user_symbol,user_timeframe,user_period); Print("WPRReversal Strategy Loaded."); } //+------------------------------------------------------------------+ //| Our class destructor | //+------------------------------------------------------------------+ WPRReversal::~WPRReversal() { delete my_wpr; } //+------------------------------------------------------------------+
Создание советника
Теперь мы перейдем к определению советника, который будет использоваться в нашей текущей конфигурации. Первая часть нашего советника — это системные константы, которые мы оставим фиксированными ради воспроизводимости наших тестов. Простые параметры, такие как сдвиг скользящей средней и тип скользящей средней, которую мы хотим использовать, должны быть зафиксированы. Им будут присвоены значения, которые легко запомнить, например, нулевой сдвиг.
//+------------------------------------------------------------------+ //| MSA Test 1.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ //--- Fix any parameters that can afford to remain fixed #define MA_SHIFT 0 #define MA_TYPE MODE_EMA #define RSI_PRICE PRICE_CLOSE
Кроме того, мы должны принимать определенные пользовательские данные. Помните нашу аналогию: мы намерены получать эти варианты входных данных от генетического оптимизатора. Первые три группы параметров должны быть хорошо знакомы читателю — это просто периоды, которые мы собираемся использовать для наших технических индикаторов.
Группа входных данных, которая представляет для нас особый интерес в рамках сегодняшней дискуссии,— последняя: группа глобальных параметров стратегии. Именно там будут храниться веса, которые мы устанавливаем сегодня. Такие настройки, как период удержания (holding period) и таймфрейм стратегии, уже должны быть знакомы нашим постоянным читателям. Однако новым читателям следует знать, что период удержания относится к тому, как долго мы будем ждать, прежде чем посчитать, что позиция достигла "зрелости" и должна быть закрыта. Разумеется, этот период чувствителен к таймфрейму стратегии. Например, период удержания, равный 5, на таймфрейме M10 означает, что мы будем удерживать позицию в течение 50 минут перед закрытием.
//+------------------------------------------------------------------+ //| User Inputs | //+------------------------------------------------------------------+ input group "Moving Average Strategy Parameters" input int MA_PERIOD = 10;//Moving Average Period input group "RSI Strategy Parameters" input int RSI_PERIOD = 15;//RSI Period input group "WPR Strategy Parameters" input int WPR_PERIOD = 30;//WPR Period input group "Global Strategy Parameters" input ENUM_TIMEFRAMES STRATEGY_TIME_FRAME = PERIOD_D1;//Strategy Timeframe input int HOLDING_PERIOD = 5;//Position Maturity Period input double weight_1 = 0.5;//Strategy 1 vote weight input double weight_2 = 0.5;//Strategy 2 vote weight input double weight_3 = 0.5;//Strategy 3 vote weight
Далее следуют зависимости, которые потребуются нашему торговому приложению. Первая из них — это стандартная торговая библиотека, которая является нашей базовой зависимостью. Она помогает нам управлять позициями. Кроме того, у нас есть такие пользовательские зависимости, как Time и TradeInfo: они помогают нам определять моменты, когда можно реагировать на рыночную информацию, а также предоставляют доступ к уровням стопов, ценам Ask и минимально допустимым объемам сделок соответственно.
Оставшиеся три зависимости — это классы стратегий, которые мы создавали на протяжении всей этой серии статей. Они уже должны быть вам знакомы, а если вы новый читатель, то как минимум последняя зависимость должна быть узнаваема, так как это именно то, что мы построили вместе сегодня.
//+------------------------------------------------------------------+ //| Dependencies | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> #include <VolatilityDoctor\Time\Time.mqh> #include <VolatilityDoctor\Trade\TradeInfo.mqh> #include <VolatilityDoctor\Strategies\OpenCloseMACrossover.mqh> #include <VolatilityDoctor\Strategies\RSIMidPoint.mqh> #include <VolatilityDoctor\Strategies\WPRReversal.mqh>
Нам также потребуется несколько глобальных переменных, таких как хендлеры (дескрипторы) для наших кастомных объектов и таймер, который поможет нам отслеживать, насколько мы близки к моменту закрытия наших позиций.
//+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ //--- Custom Types CTrade Trade; Time *TradeTime; TradeInfo *TradeInformation; RSIMidPoint *RSIMid; OpenCloseMACrossover *MACross; WPRReversal *WPRR; //--- System Types int position_timer;
При первом запуске нашего приложения мы создадим новые экземпляры наших пользовательских классов — таких как стратегии и класс TradeInfo.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Create dynamic instances of our custom types TradeTime = new Time(Symbol(),STRATEGY_TIME_FRAME); TradeInformation = new TradeInfo(Symbol(),STRATEGY_TIME_FRAME); MACross = new OpenCloseMACrossover(Symbol(),STRATEGY_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE); RSIMid = new RSIMidPoint(Symbol(),STRATEGY_TIME_FRAME,RSI_PERIOD,RSI_PRICE); WPRR = new WPRReversal(Symbol(),STRATEGY_TIME_FRAME,WPR_PERIOD); //--- Everything was fine return(INIT_SUCCEEDED); } //--- End of OnInit Scope
Когда приложение больше не используется, мы удаляем эти пользовательские объекты, чтобы гарантировать отсутствие утечек памяти.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Delete the dynamic objects delete TradeTime; delete TradeInformation; delete MACross; delete RSIMid; } //--- End of Deinit Scope
При получении новых ценовых данных мы первым делом проверяем, сформировалась ли новая свеча. Если это так, мы обновляем параметры и значения индикаторов во всех наших стратегиях. Наконец, если у нас нет открытых позиций, мы сбрасываем таймер и проверяем условия для входа (сигналы). В противном случае, если позиции уже открыты, мы отслеживаем, насколько мы близки к моменту их "зрелости", готовясь к закрытию сделки.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Check if a new daily candle has formed if(TradeTime.NewCandle()) { //--- Update strategy Update(); //--- If we have no open positions if(PositionsTotal() == 0) { //--- Reset the position timer position_timer = 0; //--- Check for a trading signal CheckSignal(); } //--- Otherwise else { //--- The position has reached maturity if(position_timer == HOLDING_PERIOD) Trade.PositionClose(Symbol()); //--- Otherwise keep holding else position_timer++; } } } //--- End of OnTick Scope
Метод обновления реализован путем простого вызова соответствующей функции обновления для каждой из наших стратегий.
//+------------------------------------------------------------------+ //| Update our technical indicators | //+------------------------------------------------------------------+ void Update(void) { //--- Update the strategy RSIMid.Update(); MACross.Update(); WPRR.Update(); } //--- End of Update Scope
Метод check_signal интересен тем, как он устроен. Мы начинаем с того, что инициализируем переменную общего голосования нулевым значением. Если по завершении процесса общая сумма голосов оказывается положительной, мы открываем длинную позицию. Если же итоговая сумма отрицательная, мы открываем позицию на продажу. В ходе этого процесса мы проверяем, какой сигнал генерирует каждая отдельная стратегия: Если стратегия выдает сигнал на покупку, мы прибавляем вес этой стратегии к общему количеству голосов. Если она выдает сигнал на продажу, мы вычитаем её вес из общей суммы. Каждой стратегии дается ровно одна возможность проголосовать. В конце мы оцениваем итоговый результат согласно описанным правилам.
//+------------------------------------------------------------------+ //| Check for a trading signal using our cross-over strategy | //+------------------------------------------------------------------+ void CheckSignal(void) { double vote = 0; if(MACross.BuySignal()) vote += weight_1; else if(MACross.SellSignal()) vote -= weight_1; if(RSIMid.BuySignal()) vote += weight_2; else if(RSIMid.SellSignal()) vote -= weight_2; if(WPRR.BuySignal()) vote += weight_3; else if(WPRR.SellSignal()) vote -= weight_3; //--- Long positions when the close moving average is above the open if(vote > 0) { Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,""); return; } //--- Otherwise short else if(vote < 0) { Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,""); return; } } //--- End of CheckSignal Scope
Как и в случае с любым другим приложением, мы должны завершить работу, в завершение снимем определения системных констант.
//+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef MA_SHIFT #undef RSI_PRICE #undef MA_TYPE //+------------------------------------------------------------------+
Мы готовы приступить к тестированию и оптимизации нашей торговой стратегии. Начнем с выбора эксперта, которого мы только что создали. Далее укажем символ, на котором будет проводиться тестирование. На протяжении всего нашего обсуждения мы использовали пару EURUSD на дневном таймфрейме, как и было оговорено ранее. Даты тестирования будут выбраны в режиме "Кастомный интервал" (Custom period): мы проводим тесты с февраля 2023 года по май 2025 года.

Рисунок 3: Настройки и даты, которые мы будем использовать для нашей генетической оптимизации
Для форвардного тестирования мы выбираем половину данных — это означает, что первая половина будет использована для бэктеста, а вторая — для форвард-теста. Форвард-тест предназначен для того, чтобы показать, какие стратегии стабильны, а какие, скорее всего, просто "подстроились" под исторические данные (переобучены). Мы всегда выбираем случайную задержку для наиболее аутентичной симуляции рыночных событий, а наше моделирование должно основываться на реальных тиках.

Рисунок 4: Инструктаж генетического оптимизатора относительно того, какие значения и в каких интервалах он должен искать параметры нашей стратегии
Наконец, для оптимизации выберите "быстрый генетический алгоритм". Общее количество параметров в нашей стратегии — это тот аспект, который мы изо всех сил старались контролировать и ограничивать. Однако, как мы видим, общее число шагов, необходимых для оптимизации стратегии — даже при весьма скромных настройках — значительно выросло. Оно действительно стало огромным всего за один этап. Поэтому я счел необходимым переложить часть работы на MQL5 Cloud. Чтобы последовать этому примеру, вы должны сначала войти в свой аккаунт MQL5 и иметь положительный баланс.

Рисунок 5: Вход в учетную запись пользователя MQL5 через терминал MetaTrader 5
Просто запустите процедуру оптимизации, затем щелкните правой кнопкой мыши на количестве ядер, доступных на вашем компьютере, и выберите "Использовать сеть MQL5 Cloud Network". Все это делается на вкладке "Агенты" (Agents) вашего тестера стратегий.

Рисунок 6: Включение облака MQL5 для ускорения бэктестинга
Как только вы включите MQL5 Cloud, часть задач, выполняемых на вашей машине, будет переложена на облако. Это поможет ускорить процедуру оптимизации, и, будем надеяться, мы получим результаты быстрее — при условии, что соединение с сетью защищено и надежно.

Рисунок 7: Подключение к любому из доступных дата-центров рядом с вами
Результаты оптимизации представляют собой тесты, проведенные на исторических данных, доступных генетическому оптимизатору. Это позволяет ему оценить, насколько эффективно работает стратегия, и соответствующим образом скорректировать параметры для улучшения показателей. Однако у генетического оптимизатора нет доступа к результатам форвард-теста — они отражают то, как выбранные настройки стратегии ведут себя на данных "вне выборки".
Глядя на результаты бэктеста, мы видим, что уровни прибыли соответствуют тем, что были достигнуты в предыдущих версиях нашего торгового приложения. Когда мы анализируем наиболее эффективные проходы (топ-стратегии), мы замечаем, что веса, присвоенные каждой суб-стратегии, находятся в диапазоне от 0.4 до 0.8. Эти значения весов для лучших конфигураций довольно близки друг к другу. Это говорит о том, что наиболее прибыльная настройка в бэктесте использовала все стратегии одновременно, а не полагалась на какую-то одну.

Рисунок 8: Результаты бэктеста нашего процесса генетической оптимизации
Однако когда мы переходим к форвард-тесту, мы видим, что наиболее эффективные конфигурации в основном полагались только на две стратегии. Фактически, топовые проходы имели минимальные веса для Третьей стратегии — некоторые даже присвоили ей нулевой вес.
Что удручает, так это то, что лишь немногие из наиболее эффективных стратегий в форвард-тесте были прибыльными и в бэктесте. Тем не менее, среди тех конфигураций, которые показали себя хорошо в обоих тестах, мы снова заметили, что Третья стратегия имела малые веса — даже в самых стабильных наборах параметров, которые мы смогли найти.
Это подталкивает нас к идее отказаться от Третьей стратегии, так как она не внесла значимого вклада в наиболее успешные результаты форвард-теста. Однако этот вывод основан на самой прибыльной из найденных конфигураций, а принимать решения таким образом не всегда целесообразно, так как это может привести к "переобучению" наших решений под конкретные имеющиеся данные.
Тем не менее, если взглянуть на все стратегии, которые были прибыльными в обоих тестах, мы в целом обнаружим, что в большинстве случаев все три стратегии имели веса, относительно близкие друг к другу. И только в этом одном выдающемся случае Третья стратегия имела наименьший вес и наилучшие показатели. Принимать решения в условиях такой неопределенности — сложная задача. Однако такова природа вызова, стоящего перед нами.
Поэтому, полагая, что наши действия направлены на достижение максимально возможной производительности, мы сделаем вывод, что Третья стратегия, возможно, не так уж важна, и продолжим использовать только первые две.

Рисунок 9: Результаты форвардного тестирования в процессе генетической оптимизации подсказывают нам, что Стратегия 3, возможно, не так уж важна для нашего успеха
Заключение
Как мы увидели из нашего обсуждения, определение оптимального количества стратегий в ансамбле — достаточно сложная задача. Мы не всегда знаем заранее, потребуется ли нам одна, пять или десять различных стратегий.
Однако главный вывод заключается в том, что генетический оптимизатор помогает нам с легкостью решать такие трудные вопросы. Стоит также отметить, что генетический оптимизатор можно считать гораздо более мощным инструментом, чем популярный ChatGPT и другие языковые модели (LLM), на которые разработчики могут полагаться при создании алгоритмических приложений.
Как мы уже обсуждали ранее в нашей сопутствующей серии статей "Преодоление ограничений ИИ", мы заметили, что алгоритмы, ориентированные на конкретную область, по своей природе более ценны для нас, чем алгоритмы общего назначения. ChatGPT и другие подобные LLM являются алгоритмами общего назначения, тогда как генетический оптимизатор, встроенный в вашу копию MetaTrader 5, является алгоритмом, ориентированным на предметную область, что делает его гораздо более подходящим инструментом для такой задачи по сравнению с попытками задать тот же вопрос ChatGPT.
Мы также продемонстрировали, как можно начать использовать MQL5 Cloud для ускорения процессов бэктестинга и оптимизации. Возможно, эта статья не была бы завершена вовремя без использования MQL5 Cloud. Начать работу очень просто, а стоимость использования весьма доступна.
Фактически, вы получаете 24-часовой доступ к облачным вычислениям с несколькими резервируемыми дата-центрами. Даже если вы потеряете соединение из-за проблем с сетью, связь почти всегда будет сохраняться. В целом, MQL5 Cloud и генетический оптимизатор являются незаменимыми инструментами в арсенале современного разработчика алгоритмических систем.
Это упражнение поможет нам направлять процесс принятия решений по мере дальнейшей разработки статистических моделей для нашей торговой стратегии.
В последующих обсуждениях мы уже узнаем, что первых двух стратегий может быть достаточно для построения задачи бинарной классификации, в которой либо Стратегия 1 является наиболее прибыльной, либо Стратегия 2. Затем мы сравним производительность нашего приложения статистического моделирования с исходной стратегией, которую разработали вместе.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/18770
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Преодоление проблем доступности в торговых инструментах на MQL5 (Часть I): Как добавить контекстные голосовые оповещения в индикаторы MQL5
Нейросети в трейдинге: Многодоменная архитектура анализа финансовых данных (Окончание)
Создание самооптимизирующихся советников на MQL5 (Часть 9): Двойное пересечение скользящих средних
Статистический арбитраж на основе коинтегрированных акций (Часть 1): Tесты Энгла — Грейнджера и Йохансена на коинтеграцию
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования