Автоматизация торговых стратегий на MQL5 (Часть 11): Разработка многоуровневой системы сеточной торговли
Введение
В своей предыдущей статье (Часть 10), мы разработали советник для автоматизации стратегии Trend Flat Momentum с использованием сочетания скользящих средних и фильтров импульсов на языке MetaQuotes Language 5 (MQL5). Теперь, в Части 11, мы сосредоточимся на построении многоуровневой системы сеточной торговли, использующей многоуровневый сеточный подход для извлечения прибыли из колебаний рынка. Статья структурирована по следующим темам:
- Введение
- Знакомство с архитектурой многоуровневой сеточной системы
- Реализация средствами MQL5
- Тестирование на истории
- Заключение
К концу настоящей статьи у вас будет полное представление и полнофункциональная программа, готовая к реальной торговле. Давайте погрузимся в процесс!
Знакомство с архитектурой многоуровневой сеточной системы
Многоуровневая сеточная торговая система - это структурированный подход, извлекющий выгоду из волатильности рынка путем размещения серии ордеров на покупку и продажу с заранее определенными интервалами в диапазоне ценовых уровней. Стратегия, которую мы собираемся реализовать, направлена не на предсказание направления движения рынка, а скорее на получение прибыли от естественного движения цен, независимо от того, движется ли рынок вверх, вниз или в боковом направлении.
Основываясь на этой концепции, наша программа будет реализовывать многоуровневую сеточную стратегию с помощью модульной конструкции, которая разделяет обнаружение сигналов, исполнение ордеров и управление рисками. При разработке нашей системы мы сначала инициализируем ключевые параметры, такие как скользящие средние для определения торговых сигналов, и настроим структуру корзины, которая включает в себя такие детали торговли, как начальный размер лота, шаг сетки и уровни тейк—профита.
По мере развития рынка программа будет отслеживать движение цен, чтобы инициировать новые сделки и управлять существующими позициями, добавляя ордера на каждом уровне сетки на основе заранее определенных условий и динамически корректируя параметры риска. Архитектура также будет включать функции для пересчета точек безубыточности, изменения целевых показателей тейк-профита и закрытия позиций при достижении целевых показателей прибыли или пороговых значений риска. Этот структурированный план не только разделит программу на отдельные, управляемые компоненты, но и обеспечит, чтобы каждый уровень сетки вносил свой вклад в согласованную торговую стратегию с управлением рисками, готовую к надежному тестированию на истории и внедрению в торговлю. В двух словах, вот как будет выглядеть архитектура.

Реализация средствами 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" - в противном случае. После запуска программы получаем следующий результат.

На изображении видно, что мы подтвердили начальные позиции в соответствии с реализованными корзинами или сигналами. Затем нам нужно перейти к управлению этими позициями, управляя каждой корзиной индивидуально. Для достижения этой цели мы используем 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, чтобы избежать ошибок при делении на ноль. После запуска программы получаем следующий результат.

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

На рисунке видно, что как только мы открываем позицию на покупку в сетке для корзины 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(), чтобы настроить размер массива, предотвращая ненужное использование памяти и гарантируя отслеживание только активных корзин. Такой подход позволяет эффективно управлять корзинами и предотвращает беспорядок в системе. После выполнения получаем следующий результат.

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

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

Заключение
В заключение отметим, что нами разработан многоуровневый советник для сеточной торговли на MQL5, который эффективно управляет многоуровневыми торговыми операциями, динамической корректировкой сетки и структурированным восстановлением. Благодаря масштабируемому шагу сетки, контролируемому движению лотов и выходу из безубыточности, система адаптируется к колебаниям рынка, оптимизируя при этом риск и прибыль.
Отказ от ответственности: Содержание настоящей статьи предназначено только для целей обучения. Торговля сопряжена со значительным финансовым риском, а рыночные условия могут быть непредсказуемыми. Перед началом использования в реальных условиях необходимы тщательное тестирование на истории и управление рисками.
Применяя эти методы, вы сможете улучшить свои навыки алгоритмической торговли и усовершенствовать основанную на сетке стратегию. Продолжайте тестировать и оптимизировать систему для достижения долгосрочного успеха. Желаем удачи!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17350
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Разработка инструментария для анализа движения цен (Часть 10): Внешние библиотеки (II) VWAP
От начального до среднего уровня: Индикатор (II)
Нейросети в трейдинге: Обучение глубоких спайкинговых моделей (Окончание)
Разрабатываем менеджер терминалов (Часть 1): Постановка задачи
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Очень хороший код и очень быстрый советник!
К сожалению, есть проблема с расчетом размера лота - множители с десятичной дробью (например, 1.3, 1.5 и т.д.) могут вызвать проблемы с функциями ордеров MQL, так как размер лота иногда выдает код ошибки 4756, когда множитель не равен 1 или 2.
Было бы очень хорошо, если бы расчет размера партии можно было незначительно изменить, чтобы обеспечить правильный расчет размера партии для передачи в функции заказа для всех значений множителей.
Было бы очень хорошо, если бы расчет размера партии можно было незначительно изменить, чтобы обеспечить правильный расчет размера партии для передачи в функции заказа для всех значений множителей.
Спасибо за добрый отзыв. Конечно.
Привет,
Прочитав статью, нашел ее полезной и обязательно протестирую. Однако, кажется, я не вижу или, возможно, я пропустил из статьи о разделении первой позиции TP, который я считаю, что это также полезно и устойчиво для торговой стратегии.
Спасибо.
Спасибо.
Конечно, спасибо.