English 中文 Español Deutsch 日本語
preview
Автоматизация торговых стратегий на MQL5 (Часть 11): Разработка многоуровневой системы сеточной торговли

Автоматизация торговых стратегий на MQL5 (Часть 11): Разработка многоуровневой системы сеточной торговли

MetaTrader 5Трейдинг |
483 4
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В своей предыдущей статье (Часть 10), мы разработали советник для автоматизации стратегии Trend Flat Momentum с использованием сочетания скользящих средних и фильтров импульсов на языке MetaQuotes Language 5 (MQL5). Теперь, в Части 11, мы сосредоточимся на построении многоуровневой системы сеточной торговли, использующей многоуровневый сеточный подход для извлечения прибыли из колебаний рынка. Статья структурирована по следующим темам:

  1. Введение
  2. Знакомство с архитектурой многоуровневой сеточной системы
  3. Реализация средствами MQL5
  4. Тестирование на истории
  5. Заключение

К концу настоящей статьи у вас будет полное представление и полнофункциональная программа, готовая к реальной торговле. Давайте погрузимся в процесс!


Знакомство с архитектурой многоуровневой сеточной системы

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

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

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

GRIDS ARCHITECTURE



Реализация средствами MQL5

Чтобы создать программу на MQL5, откройте  MetaEditor, перейдите в Навигатор, найдите папку «Индикаторы» (Indicators), перейдите на вкладку "Создать" (New) и следуйте инструкциям по созданию файла. Как только это будет сделано, в среде программирования нам нужно будет объявить некоторые  метаданные, которые будут использоваться во всей программе.

//+------------------------------------------------------------------+
//|                        Copyright 2025, Forex Algo-Trader, Allan. |
//|                                 "https://t.me/Forex_Algo_Trader" |
//+------------------------------------------------------------------+
#property copyright "Forex Algo-Trader, Allan"
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property description "This EA trades multiple signals with grid strategy using baskets"
#property strict

#include <Trade/Trade.mqh> //--- Includes the standard trading library for executing trades
CTrade obj_Trade; //--- Instantiates the CTrade object used for managing trade operations

//--- Closure Mode Enumeration and Inputs
enum ClosureMode {
   CLOSE_BY_PROFIT,      //--- Use total profit (in currency) to close positions
   CLOSE_BY_POINTS       //--- Use points threshold from breakeven to close positions
};

input group "General EA Settings"
input ClosureMode closureMode = CLOSE_BY_POINTS;
input double inpLotSize = 0.01;
input long inpMagicNo = 1234567;
input int inpTp_Points = 100;
input int inpGridSize = 100;
input double inpMultiplier = 2.0;
input int inpBreakevenPts = 50;
input int maxBaskets = 5;

input group "MA Indicator Settings" //--- Begins the input group for Moving Average indicator settings
input int inpMAPeriod = 21;                         //--- Period used for the Moving Average calculation

Здесь мы устанавливаем основополагающие компоненты нашей программы, обеспечивающие бесперебойное совершение сделок и стратегическое управление позициями. Начинаем с включения библиотеки "Trade/Trade.mqh", предоставляющей доступ к основным функциям исполнения сделок. Для облегчения торговых операций создаем экземпляр объекта "CTrade" как "obj_Trade", что позволяет нам эффективно размещать, изменять и закрывать ордера в рамках нашей автоматизированной стратегии.

Определяем перечисление "ClosureMode" для обеспечения гибкости в управлении завершением сделок. Программа может работать в двух режимах: "CLOSE_BY_PROFIT", который запускает закрытие, когда общая накопленная прибыль достигает заданного порога в валюте счета, и "CLOSE_BY_POINTS", который закрывает позиции на заранее определенном расстоянии от уровня безубыточности. Это гарантирует, что пользователь может динамически корректировать свою стратегию выхода, основываясь на поведении рынка и толерантности к риску.

Далее вводим структурированный раздел входные данные в разделе "Общие настройки советника", позволяющий настраивать торговую стратегию по усмотрению пользователя. Указываем "inpLotSize" для контроля начального объема торговли и используем "inpMagicNo" для уникальной идентификации сделок советника, предотвращая конфликты с другими активными стратегиями. Для исполнения на основе сетки устанавливаем "inpTp_Points" для определения уровня тейк-профита для каждой сделки, в то время как "inpGridSize" определяет интервал между последовательными сеточными ордерами. Параметр "inpMultiplier" постепенно увеличивает размер сделок, реализуя адаптивное расширение сетки для максимизации потенциальной прибыли при одновременном управлении рисками. Для дальнейшего улучшения контроля рисков настраиваем "inpBreakevenPts", который переводит сделки в безубыток после определенного порога, и "maxBaskets", который ограничивает количество независимых сеточных структур, которыми советник может управлять одновременно.

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

//--- Basket Structure
struct BasketInfo {
   int basketId;            //--- Unique basket identifier (e.g., 1, 2, 3...)
   long magic;              //--- Unique magic number for this basket to differentiate its trades
   int direction;           //--- Direction of the basket: POSITION_TYPE_BUY or POSITION_TYPE_SELL
   double initialLotSize;   //--- The initial lot size assigned to the basket
   double currentLotSize;   //--- The current lot size for subsequent grid trades
   double gridSize;         //--- The next grid level price for the basket
   double takeProfit;       //--- The current take-profit price for the basket
   datetime signalTime;     //--- Timestamp of the signal to avoid duplicate trade entries
};

Здесь мы определяем структуру "BasketInfo", которая позволяет независимо организовывать каждую сеточную корзину и управлять ею. Мы присваиваем уникальный идентификатор "basketId" для отслеживания каждой корзины и используем "магию", чтобы наши сделки отличались от других. Определяем направление торговли с помощью кнопки "direction", решая, используем ли мы стратегию покупки или продажи.

Мы устанавливаем "initialLotSize" для первой сделки в корзине, в то время как "currentLotSize" динамически настраивается для последующих сделок. Используем "gridSize" для определения интервала между сделками и "takeProfit" для определения нашей целевой прибыли. Для предотвращения дублирования записей отслеживаем время подачи сигнала с помощью "signalTime". Затем можем объявить массив хранения, используя определенную структуру и некоторые начальные глобальные переменные.

BasketInfo baskets[];       //--- Dynamic array to store active basket information
int nextBasketId = 1;       //--- Counter for assigning unique IDs to new baskets
long baseMagic = inpMagicNo;//--- Base magic number obtained from user input
double takeProfitPts = inpTp_Points * _Point; //--- Convert take profit points into price units
double gridSize_Spacing = inpGridSize * _Point; //--- Convert grid size spacing from points into price units
double profitTotal_inCurrency = 100; //--- Target profit in account currency for closing positions

//--- Global Variables
int totalBars = 0;          //--- Stores the total number of bars processed so far
int handle;                 //--- Handle for the Moving Average indicator
double maData[];            //--- Array to store Moving Average indicator data

Используем динамический массив "baskets[]" для хранения информации об активных корзинах, что позволяет нам эффективно отслеживать несколько позиций. Переменная "nextBasketId" присваивает уникальные идентификаторы каждой новой корзине, в то время как "baseMagic" гарантирует, что все сделки в системе будут различимы с помощью определенного пользователем магического числа. Преобразуем вводимые пользователем данные в ценовые единицы, умножая "inpTp_Points" и "inpGridSize" на "_Point", что позволяет точно контролировать "takeProfitPts" и "gridSize_Spacing". Переменная "profitTotal_inCurrency" определяет порог прибыли, необходимый для закрытия всех позиций при использовании режима закрытия на основе валюты.

Для технического анализа инициализируем "totalBars" для отслеживания количества обработанных ценовых баров, "handle" для хранения хэндла индикатора скользящей средней и "maData[]" в качестве массива для хранения вычисленных значений скользящей средней. Таким образом, мы можем определить несколько прототипов произвольных функций, которые при необходимости будем использовать во всей программе.

//--- Function Prototypes
void InitializeBaskets(); //--- Prototype for basket initialization function (if used)
void CheckAndCloseProfitTargets(); //--- Prototype to check and close positions if profit target is reached
void CheckForNewSignal(double ask, double bid); //--- Prototype to check for new trading signals based on price
bool ExecuteInitialTrade(int basketIdx, double ask, double bid, int direction); //--- Prototype to execute the initial trade for a basket
void ManageGridPositions(int basketIdx, double ask, double bid); //--- Prototype to manage and add grid positions for an active basket
void UpdateMovingAverage(); //--- Prototype to update the Moving Average indicator data
bool IsNewBar(); //--- Prototype to check whether a new bar has formed
double CalculateBreakevenPrice(int basketId); //--- Prototype to calculate the weighted breakeven price for a basket
void CheckBreakevenClose(int basketIdx, double ask, double bid); //--- Prototype to check and close positions based on breakeven criteria
void CloseBasketPositions(int basketId); //--- Prototype to close all positions within a basket
string GetPositionComment(int basketId, bool isInitial); //--- Prototype to generate a comment for a position based on basket and trade type
int CountBasketPositions(int basketId); //--- Prototype to count the number of open positions in a basket

Здесь мы определяем прототипы функций, которые описывают основные операции нашей многоуровневой сеточной торговой системы. Эти функции обеспечат модулярность, что позволит нам эффективно структурировать исполнение сделок, управление позициями и управление рисками. Начинаем с "InitializeBaskets()", которая подготавливает систему к отслеживанию активных корзин. Функция "CheckAndCloseProfitTargets()" гарантирует, что позиции будут закрыты после выполнения заранее определенных условий получения прибыли. Чтобы обнаружить торговые возможности, "CheckForNewSignal()" оценивает уровни цен, чтобы определить, следует ли подавать новый торговый сигнал.

Функция "ExecuteInitialTrade()" будет управлять первой сделкой в корзине, в то время как "ManageGridPositions()" обеспечит систематическое расширение уровней сетки по мере движения рынка. "UpdateMovingAverage()" извлекает и обрабатывает данные индикатора скользящей средней для поддержки генерации сигналов. Для управления сделками "IsNewBar()" помогает оптимизировать исполнение, гарантируя, что действия выполняются только на основе свежих ценовых данных. "CalculateBreakevenPrice()" вычисляет взвешенную цену безубыточности для корзины, в то время как "CheckBreakevenClose()" определяет, выполняются ли условия для закрытия позиций на основе критериев безубыточности.

Для управления позициями в корзине функция "CloseBasketPositions()" облегчает контролируемый выход, гарантируя, что все позиции в корзине при необходимости будут закрыты. "GetPositionComment()" предоставляет структурированные комментарии по сделкам, улучшая отслеживание сделок, а "CountBasketPositions()" помогает отслеживать количество активных позиций в корзине, гарантируя, что система работает в рамках определенных пределов риска.

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

//+------------------------------------------------------------------+
//--- Expert initialization function
//+------------------------------------------------------------------+
int OnInit() {
   handle = iMA(_Symbol, _Period, inpMAPeriod, 0, MODE_SMA, PRICE_CLOSE); //--- Initialize the Moving Average indicator with specified period and parameters
   if(handle == INVALID_HANDLE) {
      Print("ERROR: Unable to initialize Moving Average indicator!"); //--- Log error if indicator initialization fails
      return(INIT_FAILED); //--- Terminate initialization with a failure code
   }
   ArraySetAsSeries(maData, true); //--- Set the moving average data array as a time series (newest data at index 0)
   ArrayResize(baskets, 0); //--- Initialize the baskets array as empty at startup
   obj_Trade.SetExpertMagicNumber(baseMagic); //--- Set the default magic number for trade operations
   return(INIT_SUCCEEDED); //--- Signal that initialization completed successfully
}

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

Далее настраиваем массив данных скользящей средней с помощью функции ArraySetAsSeries,  гарантируя, что самые последние значения хранятся с индексом 0 для эффективного доступа. Затем изменяем размер массива "корзины" на ноль, подготавливая его к динамическому распределению по мере открытия новых сделок. Наконец, мы присваиваем торговому объекту базовое магическое число посредством используя метода "SetExpertMagicNumber()", позволяющий советнику отслеживать сделки и управлять ими с помощью уникального идентификатора. Если все компоненты успешно инициализированы, возвращаем INIT_SUCCEEDED для подтверждения того, что советник готов к началу работы.

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

//+------------------------------------------------------------------+
//--- Expert deinitialization function
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   IndicatorRelease(handle); //--- Release the indicator handle to free up resources when the EA is removed
}

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

//+------------------------------------------------------------------+
//--- Expert tick function
//+------------------------------------------------------------------+
void OnTick() {
   if(IsNewBar()) { //--- Execute logic only when a new bar is detected

   }
}

Прототип функции выглядит следующим образом.

//+------------------------------------------------------------------+
//--- Check for New Bar
//+------------------------------------------------------------------+
bool IsNewBar() {
   int bars = iBars(_Symbol, _Period); //--- Get the current number of bars on the chart for the symbol and period
   if(bars > totalBars) { //--- Compare the current number of bars with the previously stored total
      totalBars = bars; //--- Update the stored bar count to the new value
      return true; //--- Return true to indicate a new bar has formed
   }
   return false; //--- Return false if no new bar has been detected
}

Здесь мы определяем функцию "IsNewBar()", которая проверяет, сформировался ли на графике новый бар, что необходимо для обеспечения того, чтобы наш советник обрабатывал новые ценовые данные только при появлении нового бара, предотвращая ненужные пересчеты. Начинаем с получения текущего количества баров на графике с помощью функции iBars,  которая предоставляет общее количество исторических баров для активного инструмента и таймфрейма. Затем сравниваем это значение с переменной "totalBars", которая хранит ранее записанное количество баров.

Если текущее количество баров больше значения, сохраненного в переменной "totalBars", это означает, что появился новый бар. В этом случае обновляем переменную "totalBars", используя новое значение count, и возвращаем значение "true", сигнализируя о том, что советник должен перейти к расчетам на основе баров или торговой логике. Если новый бар не обнаружен, функция возвращает значение "false", гарантируя, что советник не выполняет избыточных операций на одном и том же баре.

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

//+------------------------------------------------------------------+
//--- Update Moving Average
//+------------------------------------------------------------------+
void UpdateMovingAverage() {
   if(CopyBuffer(handle, 0, 1, 3, maData) < 0) { //--- Copy the latest 3 values from the Moving Average indicator buffer into the maData array
      Print("Error: Unable to update Moving Average data."); //--- Log an error if copying the indicator data fails
   }
}

Для функции "UpdateMovingAverage()", которая гарантирует, что наш советник извлекает последние значения из индикатора скользящая средняя, используем функцию CopyBuffer() для извлечения трёх последних значений из буфера индикатора скользящая средняя и сохраняем их в массиве "maData". Параметры задают хэндл индикатора ("handle"), индекс буфера (0 для основной линии), начальную позицию (1 для пропуска текущего формирующегося бара), количество значений (3) и целевой массив ("maData").

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

UpdateMovingAverage(); //--- Update the Moving Average data for the current bar
double ask = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); //--- Get and normalize the current ask price
double bid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); //--- Get and normalize the current bid price

//--- Check for new signals and create baskets accordingly
CheckForNewSignal(ask, bid);

После получения данных индикатора, получаем текущие цены Ask и Bid, используя функцию SymbolInfoDouble() с помощью констант SYMBOL_ASK и SYMBOL_BID соответственно. Поскольку значения цены часто имеют несколько знаков после запятой, мы используем функцию NormalizeDouble с параметром _Digits, чтобы убедиться, что они корректно отформатированы в соответствии с точностью цены символа.

Наконец, вызываем функцию "CheckForNewSignal()", передавая нормализованные цены ask и bid. Ниже представлен фрагмент кода функции.

//+------------------------------------------------------------------+
//--- Check for New Crossover Signal
//+------------------------------------------------------------------+
void CheckForNewSignal(double ask, double bid) {
   double close1 = iClose(_Symbol, _Period, 1); //--- Retrieve the close price of the previous bar
   double close2 = iClose(_Symbol, _Period, 2); //--- Retrieve the close price of the bar before the previous one
   datetime currentBarTime = iTime(_Symbol, _Period, 1); //--- Get the time of the current bar

   if(ArraySize(baskets) >= maxBaskets) return; //--- Exit if the maximum allowed baskets are already active

   //--- Buy signal: current bar closes above the MA while the previous closed below it
   if(close1 > maData[1] && close2 < maData[1]) {
      //--- Check if this signal was already processed by comparing signal times in existing baskets
      for(int i = 0; i < ArraySize(baskets); i++) {
         if(baskets[i].signalTime == currentBarTime) return; //--- Signal already acted upon; exit the function
      }
      int basketIdx = ArraySize(baskets); //--- Index for the new basket equals the current array size
      ArrayResize(baskets, basketIdx + 1); //--- Increase the size of the baskets array to add a new basket
      if (ExecuteInitialTrade(basketIdx, ask, bid, POSITION_TYPE_BUY)){
         baskets[basketIdx].signalTime = currentBarTime; //--- Record the time of the signal after a successful trade
      }
   }
   //--- Sell signal: current bar closes below the MA while the previous closed above it
   else if(close1 < maData[1] && close2 > maData[1]) {
      //--- Check for duplicate signals by verifying the signal time in active baskets
      for(int i = 0; i < ArraySize(baskets); i++) {
         if(baskets[i].signalTime == currentBarTime) return; //--- Signal already acted upon; exit the function
      }
      int basketIdx = ArraySize(baskets); //--- Determine the index for the new basket
      ArrayResize(baskets, basketIdx + 1); //--- Resize the baskets array to accommodate the new basket
      if (ExecuteInitialTrade(basketIdx, ask, bid, POSITION_TYPE_SELL)){
         baskets[basketIdx].signalTime = currentBarTime; //--- Record the signal time for the new sell basket
      }
   }
}

Для функции "CheckForNewSignal()" сначала извлекаем цены закрытия двух предыдущих баров, используя функцию iClose().  Это помогает нам определить, произошло ли пересечение. Мы также используем функцию iTime(), чтобы получить временную метку самого последнего бара, гарантируя, что мы не будем обрабатывать один и тот же сигнал несколько раз.

Прежде чем продолжить, проверяем, достигло ли количество активных корзин предела "maxBaskets". Если это так, функция возвращается, чтобы предотвратить чрезмерное накопление сделок. Относительно сигналов на покупку проверяем, находится ли последняя цена закрытия выше скользящей средней, в то время как предыдущая цена закрытия была ниже нее. Если это условие пересечения выполнено, перебираем существующие корзины, чтобы убедиться, что один и тот же сигнал еще не обрабатывался. Если сигнал новый, увеличиваем размер массива "baskets", сохраняем новую корзину по следующему доступному индексу и вызываем функцию "ExecuteInitialTrade()" с ордером POSITION_TYPE_BUY.  Если сделка совершается успешно, регистрируем время подачи сигнала, чтобы предотвратить повторные записи.

Относительно сигналов на продажу проверяем, находится ли последняя цена закрытия ниже скользящей средней, в то время как предыдущая цена закрытия была вышее нее. Если это условие соблюдено и дублирующийся сигнал не найден, расширяем массив "baskets", исполняем первоначальную сделку на продажу, используя функцию "ExecuteInitialTrade()" с ордером POSITION_TYPE_SELL и сохраняем время подачи сигнала для поддержания уникальности. Функция для совершения сделок заключается в следующем.

//+------------------------------------------------------------------+
//--- Execute Initial Trade
//+------------------------------------------------------------------+
bool ExecuteInitialTrade(int basketIdx, double ask, double bid, int direction) {
   baskets[basketIdx].basketId = nextBasketId++; //--- Assign a unique basket ID and increment the counter
   baskets[basketIdx].magic = baseMagic + baskets[basketIdx].basketId * 10000; //--- Calculate a unique magic number for the basket
   baskets[basketIdx].initialLotSize = inpLotSize; //--- Set the initial lot size for the basket from input
   baskets[basketIdx].currentLotSize = inpLotSize; //--- Initialize current lot size to the same as the initial lot size
   baskets[basketIdx].direction = direction; //--- Set the trade direction (buy or sell) for the basket
   bool isTradeExecuted = false; //--- Initialize flag to track if the trade was successfully executed
   string comment = GetPositionComment(baskets[basketIdx].basketId, true); //--- Generate a comment string indicating an initial trade
   obj_Trade.SetExpertMagicNumber(baskets[basketIdx].magic); //--- Set the trade object's magic number to the basket's unique value

   if(direction == POSITION_TYPE_BUY) {
      baskets[basketIdx].gridSize = ask - gridSize_Spacing; //--- Set the grid level for subsequent buy orders below the current ask price
      baskets[basketIdx].takeProfit = ask + takeProfitPts; //--- Calculate the take profit level for the buy order
      if(obj_Trade.Buy(baskets[basketIdx].currentLotSize, _Symbol, ask, 0, baskets[basketIdx].takeProfit, comment)) {
         Print("Basket ", baskets[basketIdx].basketId, ": Initial BUY at ", ask, " | Magic: ", baskets[basketIdx].magic); //--- Log the successful buy order details
         isTradeExecuted = true; //--- Mark the trade as executed successfully
      } else {
         Print("Basket ", baskets[basketIdx].basketId, ": Initial BUY failed, error: ", GetLastError()); //--- Log the error if the buy order fails
         ArrayResize(baskets, ArraySize(baskets) - 1); //--- Remove the basket if trade execution fails
      }
   } else if(direction == POSITION_TYPE_SELL) {
      baskets[basketIdx].gridSize = bid + gridSize_Spacing; //--- Set the grid level for subsequent sell orders above the current bid price
      baskets[basketIdx].takeProfit = bid - takeProfitPts; //--- Calculate the take profit level for the sell order
      if(obj_Trade.Sell(baskets[basketIdx].currentLotSize, _Symbol, bid, 0, baskets[basketIdx].takeProfit, comment)) {
         Print("Basket ", baskets[basketIdx].basketId, ": Initial SELL at ", bid, " | Magic: ", baskets[basketIdx].magic); //--- Log the successful sell order details
         isTradeExecuted = true; //--- Mark the trade as executed successfully
      } else {
         Print("Basket ", baskets[basketIdx].basketId, ": Initial SELL failed, error: ", GetLastError()); //--- Log the error if the sell order fails
         ArrayResize(baskets, ArraySize(baskets) - 1); //--- Remove the basket if trade execution fails
      }
   }
   return (isTradeExecuted); //--- Return the status of the trade execution
}

Определяем функцию "ExecuteInitialTrade()", чтобы гарантировать, что каждая корзина имеет уникальный идентификатор, присваивает отдельное магическое число и инициализирует ключевые торговые параметры перед размещением ордера. Сначала назначаем "basketId", увеличивая значение переменной "nextBasketId". Затем генерируем уникальное магическое число для корзины, добавляя масштабированное смещение к значению "baseMagic", обеспечивая, что каждая корзина будет работать независимо. Начальный и текущий размеры лотов установлены на "inpLotSize", чтобы установить базовый размер сделки для этой корзины. "direction" сохраняется для того, чтобы различать корзины покупок и продаж.

Для обеспечения возможности идентификации сделок вызываем функцию "GetPositionComment()" для создания дескриптивного комментария и применяем магическое число корзины к объекту сделки, используя метод "SetExpertMagicNumber()". Функция определена следующим образом, где мы используем функцию StringFormat для получения комментария с помощью тернарного оператора.

//+------------------------------------------------------------------+
//--- Generate Position Comment
//+------------------------------------------------------------------+
string GetPositionComment(int basketId, bool isInitial) {
   return StringFormat("Basket_%d_%s", basketId, isInitial ? "Initial" : "Grid"); //--- Generate a standardized comment string for a position indicating basket ID and trade type
}

Если направление POSITION_TYPE_BUY, вычисляем уровень сетки, вычитая "gridSize_Spacing" из запрашиваемой цены, и определяем уровень тейк-профита, добавляя "takeProfitPts" к запрашиваемой цене. Затем используем функцию "Buy()" из класса "CTrade" для размещения ордера. В случае успешного выполнения регистрируем детали сделки с помощью функции Print() и помечаем сделку как исполненную. Если сделка завершается неудачно, регистрируем в логе ошибку с помощью функции GetLastError() и используем функцию ArrayResize(), чтобы уменьшить размер массива "baskets", удалив неудачную корзину.

В отношении сделки на продажу (POSITION_TYPE_SELL) рассчитываем уровень сетки, добавляя "gridSize_Spacing" к цене bid и определяем уровень тейк-профита, вычитая "takeProfitPts" из цены bid. Сделка исполняется с помощью функции "Sell()". Как и в случае сделок на покупку, успешное исполнение регистрируется в логе с помощью функции "Print()", а сбой приводит к появлению лога ошибок с помощью GetLastError, за которым следует изменение размера массива "baskets" с помощью "ArrayResize()" для удаления корзины с ошибкой.

Перед исполнением любой сделки функция проверяет, достаточно ли места в массиве, вызывая "ArrayResize()» для увеличения его размера. Наконец, функция возвращает "true", если сделка исполнена успешно, а "false" - в противном случае. После запуска программы получаем следующий результат.

CONFIRMED INITIAL POSITIONS IN BASKETS

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

//--- Loop through all active baskets to manage grid positions and potential closures
for(int i = 0; i < ArraySize(baskets); i++) {
   ManageGridPositions(i, ask, bid); //--- Manage grid trading for the current basket
}

Здесь мы перебираем все активные корзины, используя цикл for, обеспечив, что каждая корзина управляется соответствующим образом. Функция arraySize определяет текущее количество корзин в массиве "baskets", обеспечивая верхний предел цикла. Это гарантирует, что мы обрабатываем все существующие корзины, не выходя за границы массива. По каждой корзине вызываем функцию "ManageGridPositions()", передавая индекс корзины вместе с нормализованными ценами "ask" и "bid". Функция показана ниже.

//+------------------------------------------------------------------+
//--- Manage Grid Positions
//+------------------------------------------------------------------+
void ManageGridPositions(int basketIdx, double ask, double bid) {
   bool newPositionOpened = false; //--- Flag to track if a new grid position has been opened
   string comment = GetPositionComment(baskets[basketIdx].basketId, false); //--- Generate a comment for grid trades in this basket
   obj_Trade.SetExpertMagicNumber(baskets[basketIdx].magic); //--- Ensure the trade object uses the basket's unique magic number

   if(baskets[basketIdx].direction == POSITION_TYPE_BUY) {
      if(ask <= baskets[basketIdx].gridSize) { //--- Check if the ask price has reached the grid level for a buy order
         baskets[basketIdx].currentLotSize *= inpMultiplier; //--- Increase the lot size based on the defined multiplier
         if(obj_Trade.Buy(baskets[basketIdx].currentLotSize, _Symbol, ask, 0, baskets[basketIdx].takeProfit, comment)) {
            newPositionOpened = true; //--- Set flag if the grid buy order is successfully executed
            Print("Basket ", baskets[basketIdx].basketId, ": Grid BUY at ", ask); //--- Log the grid buy execution details
            baskets[basketIdx].gridSize = ask - gridSize_Spacing; //--- Adjust the grid level for the next potential buy order
         } else {
            Print("Basket ", baskets[basketIdx].basketId, ": Grid BUY failed, error: ", GetLastError()); //--- Log an error if the grid buy order fails
         }
      }
   } else if(baskets[basketIdx].direction == POSITION_TYPE_SELL) {
      if(bid >= baskets[basketIdx].gridSize) { //--- Check if the bid price has reached the grid level for a sell order
         baskets[basketIdx].currentLotSize *= inpMultiplier; //--- Increase the lot size based on the multiplier for grid orders
         if(obj_Trade.Sell(baskets[basketIdx].currentLotSize, _Symbol, bid, 0, baskets[basketIdx].takeProfit, comment)) {
            newPositionOpened = true; //--- Set flag if the grid sell order is successfully executed
            Print("Basket ", baskets[basketIdx].basketId, ": Grid SELL at ", bid); //--- Log the grid sell execution details
            baskets[basketIdx].gridSize = bid + gridSize_Spacing; //--- Adjust the grid level for the next potential sell order
         } else {
            Print("Basket ", baskets[basketIdx].basketId, ": Grid SELL failed, error: ", GetLastError()); //--- Log an error if the grid sell order fails
         }
      }
   }

   //--- If a new grid position was opened and there are multiple positions, adjust the take profit to breakeven
   if(newPositionOpened && CountBasketPositions(baskets[basketIdx].basketId) > 1) {
      double breakevenPrice = CalculateBreakevenPrice(baskets[basketIdx].basketId); //--- Calculate the weighted breakeven price for the basket
      double newTP = (baskets[basketIdx].direction == POSITION_TYPE_BUY) ?
                     breakevenPrice + (inpBreakevenPts * _Point) : //--- Set new TP for buy positions
                     breakevenPrice - (inpBreakevenPts * _Point);  //--- Set new TP for sell positions
      baskets[basketIdx].takeProfit = newTP; //--- Update the basket's take profit level with the new value
      for(int j = PositionsTotal() - 1; j >= 0; j--) { //--- Loop through all open positions to update TP where necessary
         ulong ticket = PositionGetTicket(j); //--- Get the ticket number for the current position
         if(PositionSelectByTicket(ticket) && 
            PositionGetString(POSITION_SYMBOL) == _Symbol && 
            PositionGetInteger(POSITION_MAGIC) == baskets[basketIdx].magic) { //--- Identify positions that belong to the current basket
            if(!obj_Trade.PositionModify(ticket, 0, newTP)) { //--- Attempt to modify the position's take profit level
               Print("Basket ", baskets[basketIdx].basketId, ": Failed to modify TP for ticket ", ticket); //--- Log error if modifying TP fails
            }
         }
      }
      Print("Basket ", baskets[basketIdx].basketId, ": Breakeven = ", breakevenPrice, ", New TP = ", newTP); //--- Log the new breakeven and take profit levels
   }
}

Здесь мы реализуем функцию "ManageGridPositions()" для динамического управления торговлей на основе сетки в каждой активной корзине. Мы следим за тем, чтобы новые позиции сетки исполнялись на корректных ценовых уровнях и чтобы при необходимости корректировалась прибыль. Начинаем с инициализации флага "newPositionOpened", чтобы отслеживать, была ли совершена новая сеточная сделка. Используя функцию "GetPositionComment()", мы генерируем строку комментариев, соответствующую типу сделки (initial или grid). Затем вызываем функцию "SetExpertMagicNumber()", чтобы присвоить корзине уникальное магическое число, гарантирующее надлежащее отслеживание всех сделок в корзине.

Для корзин на покупку проверяем, снизилась ли запрашиваемая цена до порогового значения "gridSize" или ниже него. Если это условие выполнено, корректируем размер лота, умножая "currentLotSize" на входной параметр "inpMultiplier". Далее пытаемся разместить ордер на покупку, используя метод "Buy()" из торгового объекта "obj_Trade". Если сделка завершается успешно, обновляем "gridSize", вычитая "gridSize_Spacing", гарантируя, что следующая сделка на покупку будет размещена корректно. Мы также регистрируем успешное выполнение с помощью функции Print(). Если ордер на покупку не исполняется, извлекаем и регистрируем ошибку, используя функцию GetLastError(). 

В отношении корзин на продажу следуем аналогичному процессу, но вместо этого проверяем, достигла ли цена bid порогового значения gridSize или превысила его. Если это условие выполнено, корректируем размер лота, применяя "inpMultiplier" к "currentLotSize". Затем исполняем ордер на продажу, используя функцию "Sell()", обновляя gridSize путем прибавления "gridSize_Spacing", чтобы определить следующий уровень продажи. Если ордер исполнен успешно, регистрируем детали с помощью "Print()", а если не исполнен, регистрируем ошибку с помощью "GetLastError()".

Если открыта новая позиция в сетке и в корзине теперь находится несколько сделок, мы корректируем тейк-профит до уровня безубыточности. Сначала определяем цену безубыточности, вызывая функцию "CalculateBreakevenPrice()". Затем расчитываем новый уровень тейк-профита, основываясь на направлении корзины:

  • Для корзин на покупку тейк-профит устанавливается путем добавления "inpBreakevenPts" (пересчитанных в ценовые пункты) к цене безубыточности.
  • Для корзин на продажу тейк-профит корректируется путем вычитания "inpBreakevenPts" из цены безубыточности.

Далее перебираем все открытые позиции, используя функцию PositionsTotal(),  извлекая номер тикета каждой позиции с помощью PositionGetTicket(). Используем PositionSelectByTicket() для выбора позиции и проверки ее символа с помощью функции "PositionGetString". Мы также проверяем принадлежность позиции к корректной корзине, проверяя ее магическое число с помощью параметра "POSITION_MAGIC". После подтверждения пытаемся изменить её тейк-профит, используя метод "PositionModify()". Если это изменение завершается неудачей, регистрируем ошибку в логе.

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

//+------------------------------------------------------------------+
//--- Calculate Weighted Breakeven Price for a Basket
//+------------------------------------------------------------------+
double CalculateBreakevenPrice(int basketId) {
   double weightedSum = 0.0; //--- Initialize sum for weighted prices
   double totalLots = 0.0;   //--- Initialize sum for total lot sizes
   for(int i = 0; i < PositionsTotal(); i++) { //--- Loop over all open positions
      ulong ticket = PositionGetTicket(i); //--- Retrieve the ticket for the current position
      if(PositionSelectByTicket(ticket) && PositionGetString(POSITION_SYMBOL) == _Symbol && 
         StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= 0) { //--- Check if the position belongs to the specified basket
         double lot = PositionGetDouble(POSITION_VOLUME); //--- Get the lot size of the position
         double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get the open price of the position
         weightedSum += openPrice * lot; //--- Add the weighted price to the sum
         totalLots += lot; //--- Add the lot size to the total lots
      }
   }
   return (totalLots > 0) ? (weightedSum / totalLots) : 0; //--- Return the weighted average price (breakeven) or 0 if no positions found
}

Мы реализуем функцию "CalculateBreakevenPrice()" для определения взвешенной цены безубыточности для данной корзины сделок, гарантируя, что уровень тейк-профита может динамически корректироваться на основе взвешенных по объему цен входа для всех открытых позиций в корзине. Начинаем с инициализации "weightedSum" для хранения суммы взвешенных цен и "totalLots" для отслеживания общего размера лота по всем позициям в корзине. Затем выполняем перебор всех открытых позиций.

Для каждой позиции мы получаем соответствующий номер тикета, используя PositionGetTicket,  и выбираем позицию с помощью PositionSelectByTicket. Проверяем, относится ли позиция к текущему торговому инструменту. Кроме того, мы проверяем, входит ли позиция в указанную корзину, путем поиска идентификатора корзины в строке комментария с помощью функции StringFind().  Комментарий должен содержать "Basket_" + IntegerToString(basketId), чтобы быть отнесенным к той же корзине.

Как только позиция подтверждена, извлекаем размер ее лота, используя PositionGetDouble(POSITION_VOLUME)", и цену открытия, используя POSITION_PRICE_OPEN. Затем умножаем цену открытия на размер лота и добавляем результат к "weightedSum", гарантируя, что бОльшие размеры лота оказывают большее влияние на конечную цену безубыточности. Одновременно суммируем общий размер лота в "totalLots".

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

GRIDS OPENED IMAGE 1

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

GRID POSITIONS JOURNAL

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

if(closureMode == CLOSE_BY_PROFIT) CheckAndCloseProfitTargets(); //--- If using profit target closure mode, check for profit conditions
if(closureMode == CLOSE_BY_POINTS && CountBasketPositions(baskets[i].basketId) > 1) {
   CheckBreakevenClose(i, ask, bid); //--- If using points-based closure and multiple positions exist, check breakeven conditions
}

Здесь мы управляем закрытием сделок на основе выбранного "closureMode". Если задано значение "CLOSE_BY_PROFIT", вызываем "CheckAndCloseProfitTargets()", чтобы закрыть корзины, которые достигли целевых показателей прибыли. Если установлено значение "CLOSE_BY_POINTS", проверяем, что в корзине есть несколько позиций, используя "CountBasketPositions()", прежде чем вызывать "CheckBreakevenClose()", чтобы закрыть сделки в безубыток при выполнении условий. Функции представлены ниже.

//+------------------------------------------------------------------+
//--- Check and Close Profit Targets (for CLOSE_BY_PROFIT mode)
//+------------------------------------------------------------------+
void CheckAndCloseProfitTargets() {
   for(int i = 0; i < ArraySize(baskets); i++) { //--- Loop through each active basket
      int posCount = CountBasketPositions(baskets[i].basketId); //--- Count how many positions belong to the current basket
      if(posCount <= 1) continue; //--- Skip baskets with only one position as profit target checks apply to multiple positions
      double totalProfit = 0; //--- Initialize the total profit accumulator for the basket
      for(int j = PositionsTotal() - 1; j >= 0; j--) { //--- Loop through all open positions to sum their profits
         ulong ticket = PositionGetTicket(j); //--- Get the ticket for the current position
         if(PositionSelectByTicket(ticket) && 
            StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(baskets[i].basketId)) >= 0) { //--- Check if the position is part of the current basket
            totalProfit += PositionGetDouble(POSITION_PROFIT); //--- Add the position's profit to the basket's total profit
         }
      }
      if(totalProfit >= profitTotal_inCurrency) { //--- Check if the accumulated profit meets or exceeds the profit target
         Print("Basket ", baskets[i].basketId, ": Profit target reached (", totalProfit, ")"); //--- Log that the profit target has been reached for the basket
         CloseBasketPositions(baskets[i].basketId); //--- Close all positions in the basket to secure the profits
      }
   }
}

Здесь мы проверяем и закрываем корзины по достижении ими целевого уровня прибыли в режиме "CLOSE_BY_PROFIT". Производим перебор "baskets" и используем "CountBasketPositions()", чтобы убедиться в существовании нескольких позиций. Затем суммируем прибыль, используя "PositionGetDouble(POSITION_PROFIT)" для всех позиций в корзине. Если общая прибыль соответствует или превышает "profitTotal_inCurrency", регистрируем событие в логе и вызываем "CloseBasketPositions()", чтобы зафиксировать прибыль. Функция "CountBasketPositions" определяется следующим образом.

//+------------------------------------------------------------------+
//--- Count Positions in a Basket
//+------------------------------------------------------------------+
int CountBasketPositions(int basketId) {
   int count = 0; //--- Initialize the counter for positions in the basket
   for(int i = 0; i < PositionsTotal(); i++) { //--- Loop through all open positions
      ulong ticket = PositionGetTicket(i); //--- Retrieve the ticket for the current position
      if(PositionSelectByTicket(ticket) && 
         StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= 0) { //--- Check if the position belongs to the specified basket
         count++; //--- Increment the counter if a matching position is found
      }
   }
   return count; //--- Return the total number of positions in the basket
}

Мы используем функцию "CountBasketPositions()" для подсчета позиций в определенной корзине. Перебираем все позиции, извлекаем каждый "тикет" с помощью функции PositionGetTicket() и проверяем, содержит ли POSITION_COMMENT идентификатор корзины. Если найдено совпадение, увеличиваем значение "count". Наконец, возвращаем общее количество позиций в корзине. Определение функции "CloseBasketPositions()" также выглядит следующим образом.

//+------------------------------------------------------------------+
//--- Close All Positions in a Basket
//+------------------------------------------------------------------+
void CloseBasketPositions(int basketId) {
   for(int i = PositionsTotal() - 1; i >= 0; i--) { //--- Loop backwards through all open positions
      ulong ticket = PositionGetTicket(i); //--- Retrieve the ticket of the current position
      if(PositionSelectByTicket(ticket) && 
         StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= 0) { //--- Identify if the position belongs to the specified basket
         if(obj_Trade.PositionClose(ticket)) { //--- Attempt to close the position
            Print("Basket ", basketId, ": Closed position ticket ", ticket); //--- Log the successful closure of the position
         }
      }
   }
}

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

//+------------------------------------------------------------------+
//--- Check Breakeven Close
//+------------------------------------------------------------------+
void CheckBreakevenClose(int basketIdx, double ask, double bid) {
   double breakevenPrice = CalculateBreakevenPrice(baskets[basketIdx].basketId); //--- Calculate the breakeven price for the basket
   if(baskets[basketIdx].direction == POSITION_TYPE_BUY) {
      if(bid >= breakevenPrice + (inpBreakevenPts * _Point)) { //--- Check if the bid price exceeds breakeven plus threshold for buy positions
         Print("Basket ", baskets[basketIdx].basketId, ": Closing BUY positions at breakeven + points"); //--- Log that breakeven condition is met for closing positions
         CloseBasketPositions(baskets[basketIdx].basketId); //--- Close all positions for the basket
      }
   } else if(baskets[basketIdx].direction == POSITION_TYPE_SELL) {
      if(ask <= breakevenPrice - (inpBreakevenPts * _Point)) { //--- Check if the ask price is below breakeven minus threshold for sell positions
         Print("Basket ", baskets[basketIdx].basketId, ": Closing SELL positions at breakeven + points"); //--- Log that breakeven condition is met for closing positions
         CloseBasketPositions(baskets[basketIdx].basketId); //--- Close all positions for the basket
      }
   }
}

Здесь мы реализуем закрытие на основе безубыточности, используя "CheckBreakevenClose()". Сначала определяем цену безубыточности с помощью функции "CalculateBreakevenPrice()". Если корзина находится в направлении BUY (на покупку), а цена bid превышает уровень безубыточности плюс определенный порог ("inpBreakevenPts * _Point"), регистрируем событие в логе и исполняем "CloseBasketPositions()" для фиксации прибыли. Аналогично, для корзин SELL (на продажу) проверяем, падает ли цена ask ниже уровня безубыточности за вычетом порога, что приводит к закрытию. Это гарантирует, что позиции будут защищены, как только движение цены выровняется с условиями безубыточности.

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

//--- Remove inactive baskets that no longer have any open positions
for(int i = ArraySize(baskets) - 1; i >= 0; i--) {
   if(CountBasketPositions(baskets[i].basketId) == 0) {
      Print("Removing inactive basket ID: ", baskets[i].basketId); //--- Log the removal of an inactive basket
      for(int j = i; j < ArraySize(baskets) - 1; j++) {
         baskets[j] = baskets[j + 1]; //--- Shift basket elements down to fill the gap
      }
      ArrayResize(baskets, ArraySize(baskets) - 1); //--- Resize the baskets array to remove the empty slot
   }
}

Здесь мы гарантируем, что неактивные корзины, в которых больше нет открытых позиций, будут эффективно удалены. Выполняем итерацию по массиву "baskets" в обратном порядке, чтобы избежать проблем со сдвигом индекса во время удаления. Используя "CountBasketPositions()", проверяем, нет ли в корзине оставшихся сделок. Если она пуста, регистрируем удаление и сдвигаем последующие элементы вниз, чтобы сохранить структуру массива. Наконец, вызываем ArrayResize(), чтобы настроить размер массива, предотвращая ненужное использование памяти и гарантируя отслеживание только активных корзин. Такой подход позволяет эффективно управлять корзинами и предотвращает беспорядок в системе. После выполнения получаем следующий результат.

CLUTTER CLEARING

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


Тестирование на истории

После тщательного тестирования в течение 1 года, в 2023 году, с использованием настроек по умолчанию, получены следующие результаты.

График тестирования на истории:

GRAPH

Отчет о тестировании на истории:

REPORT


Заключение

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

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

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

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
johnsteed
johnsteed | 12 мар. 2025 в 11:49

Очень хороший код и очень быстрый советник!

К сожалению, есть проблема с расчетом размера лота - множители с десятичной дробью (например, 1.3, 1.5 и т.д.) могут вызвать проблемы с функциями ордеров MQL, так как размер лота иногда выдает код ошибки 4756, когда множитель не равен 1 или 2.

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

Allan Munene Mutiiria
Allan Munene Mutiiria | 17 мар. 2025 в 19:19
johnsteed код ошибки 4756, когда множитель не равен 1 или 2.

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

Спасибо за добрый отзыв. Конечно.

cbkiri
cbkiri | 6 июн. 2025 в 03:18

Привет,

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


Спасибо.

Allan Munene Mutiiria
Allan Munene Mutiiria | 6 июн. 2025 в 11:19
cbkiri торговой стратегии.


Спасибо.

Конечно, спасибо.

Разработка инструментария для анализа движения цен (Часть 10): Внешние библиотеки (II) VWAP Разработка инструментария для анализа движения цен (Часть 10): Внешние библиотеки (II) VWAP
Освойте возможности VWAP с помощью нашего подробного руководства! Узнайте, как интегрировать анализ VWAP в вашу торговую стратегию, используя MQL5 и Python. Получите максимально полное представление о рынке и улучшите свои торговые решения уже сегодня.
От начального до среднего уровня: Индикатор (II) От начального до среднего уровня: Индикатор (II)
В этой статье мы рассмотрим, как реализовать расчет скользящей средней и какие меры предосторожности следует предпринять при выполнении данного расчета. Мы также поговорим о перегрузке функции OnCalculate, чтобы знать, когда и как работать с той или иной моделью.
Нейросети в трейдинге: Обучение глубоких спайкинговых моделей (Окончание) Нейросети в трейдинге: Обучение глубоких спайкинговых моделей (Окончание)
В данной статье показана практическая реализация фреймворка SEW ResNet средствами MQL5 с акцентом на прикладное применение в торговле. Двойной Bottleneck даёт возможность одновременно анализировать унитарные потоки и межканальные зависимости, не теряя градиентов при обучении. Спайковые активации с адаптивными порогами и гейты повышают устойчивость к шуму и чувствительность к новизне рынка. В тексте приведены детали реализации и результаты тестов.
Разрабатываем менеджер терминалов (Часть 1): Постановка задачи Разрабатываем менеджер терминалов (Часть 1): Постановка задачи
Как обеспечить возможность удобного контроля за несколькими терминалами, на которых торгуют советники, да ещё и на разных компьютерах? Попробуем создать веб-интерфейс по управлению запуском торговых терминалов MetaTrader 5 и просмотру детальной информации о работе каждого экземпляра.