Введение
Торговые стратегии, основанные на сетке ордеров (Grid Trading), являются популярным подходом в алгоритмической торговле. Они предполагают размещение серии отложенных или рыночных ордеров через фиксированные ценовые интервалы. Добавление элемента Мартингейла, при котором объем последующих ордеров увеличивается, призвано ускорить выход из просадки и увеличить прибыль при развороте рынка. Однако, Мартингейл значительно увеличивает требования к марже и риски, что делает проверку на достаточность маржи не просто желательной, а абсолютно необходимой.
В этой статье мы, как Senior Quant Developers, разработаем надежный экспертный советник (EA) для MQL5, который реализует стратегию Grid с Мартингейлом, уделяя особое внимание механизму проверки свободной маржи. Это поможет избежать ситуаций, когда брокер не может исполнить ордер из-за нехватки средств или, что еще хуже, когда открытые позиции приводят к стоп-ауту.
Исходный код
Ниже представлен полный исходный код экспертного советника MartingaleGridEA.mq5. Он включает в себя все необходимые функции для управления позициями, расчета лотов и, конечно же, проверки маржи.
//+------------------------------------------------------------------+
//| MartingaleGridEA.mq5 |
//| Copyright 2023, Finfluct |
//| https://finfluct.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, Finfluct"
#property link "https://finfluct.com"
#property version "1.00"
#property description "Expert Advisor for Martingale Grid strategy with Margin Check"
#property strict
//--- Input parameters
input double InitialLot = 0.01; // Начальный объем лота
input double GridStepPips = 20.0; // Шаг сетки в пунктах
input double MartingaleFactor = 1.5; // Коэффициент Мартингейла (увеличение лота)
input double TakeProfitPips = 100.0; // Тейк-профит для всей сетки в пунктах
input double StopLossPips = 0.0; // Стоп-лосс для первой позиции (0 - нет SL)
input int MaxGridLevels = 10; // Максимальное количество уровней сетки
input long MagicNumber = 12345; // Магический номер для идентификации ордеров
input ENUM_TIMEFRAME Timeframe = PERIOD_CURRENT; // Таймфрейм для работы EA
input int Slippage = 5; // Допустимое проскальзывание в пунктах
input bool EnableBuyTrades = true; // Разрешить покупки
input bool EnableSellTrades = true; // Разрешить продажи
//--- Global variables
string SymbolName;
int DigitsCount;
double PointValue;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
//--- Get symbol properties
SymbolName = _Symbol;
DigitsCount = (int)SymbolInfoInteger(SymbolName, SYMBOL_DIGITS);
PointValue = SymbolInfoDouble(SymbolName, SYMBOL_POINT);
//--- Check for valid parameters
if (InitialLot <= 0 || GridStepPips <= 0 || MartingaleFactor <= 1.0 || TakeProfitPips <= 0 || MaxGridLevels <= 0)
{
Print("ERROR: Invalid input parameters. Please check InitialLot, GridStepPips, MartingaleFactor, TakeProfitPips, MaxGridLevels.");
return INIT_PARAMETERS_INCORRECT;
}
Print("Martingale Grid EA initialized successfully on ", SymbolName);
//---
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
Print("Martingale Grid EA deinitialized. Reason: ", reason);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
//--- Check if EA is allowed to trade
if (!IsTradeAllowed())
{
Print("Trading is not allowed. Check AutoTrading button or EA properties.");
return;
}
//--- Get current price
MqlTick last_tick;
if (!SymbolInfoTick(SymbolName, last_tick))
{
Print("Error getting tick data for ", SymbolName, ". Error: ", GetLastError());
return;
}
double currentBid = last_tick.bid;
double currentAsk = last_tick.ask;
//--- Get open positions for this symbol and magic number
CArrayObj* positions = GetOpenPositions(SymbolName, MagicNumber);
int openPositionsCount = positions.Total();
//--- If no open positions, open the first one
if (openPositionsCount == 0)
{
// Determine initial trade direction (can be randomized or based on indicator)
// For simplicity, let's open a BUY if EnableBuyTrades is true, else SELL if EnableSellTrades is true
if (EnableBuyTrades)
{
if (CheckMarginSufficiency(ORDER_TYPE_BUY, InitialLot, currentAsk))
{
OpenTrade(ORDER_TYPE_BUY, InitialLot, currentAsk, TakeProfitPips, StopLossPips, MagicNumber);
}
else
{
Print("Not enough margin for initial BUY trade of ", InitialLot, " lots.");
}
}
else if (EnableSellTrades)
{
if (CheckMarginSufficiency(ORDER_TYPE_SELL, InitialLot, currentBid))
{
OpenTrade(ORDER_TYPE_SELL, InitialLot, currentBid, TakeProfitPips, StopLossPips, MagicNumber);
}
else
{
Print("Not enough margin for initial SELL trade of ", InitialLot, " lots.");
}
}
else
{
Print("Both EnableBuyTrades and EnableSellTrades are false. No trades can be opened.");
}
delete positions;
return;
}
//--- Manage existing grid
double totalProfit = 0.0;
double totalVolume = 0.0;
ENUM_ORDER_TYPE firstTradeType = WRONG_VALUE;
double firstTradePrice = 0.0;
// Iterate through positions to calculate total profit, volume, and determine grid direction
for (int i = 0; i < openPositionsCount; i++)
{
CPositionInfo* pos = positions.At(i);
if (pos == NULL) continue;
totalProfit += pos.Profit() + pos.Swap() + pos.Commission();
totalVolume += pos.Volume();
if (i == 0) // Assuming positions are sorted by open time, first one is the oldest
{
firstTradeType = pos.Type();
firstTradePrice = pos.PriceOpen();
}
}
// Check for total Take Profit for the grid
if (totalProfit >= TakeProfitPips * totalVolume * PointValue * 10) // Simplified TP check, adjust as needed
{
Print("Total grid profit reached: ", totalProfit, ". Closing all positions.");
CloseAllPositions(SymbolName, MagicNumber);
delete positions;
return;
}
// Check for new grid level
if (openPositionsCount < MaxGridLevels)
{
double lastTradePrice = GetLastPositionPrice(SymbolName, MagicNumber);
double distance = 0.0;
ENUM_ORDER_TYPE nextTradeType = WRONG_VALUE;
double nextPrice = 0.0;
if (firstTradeType == ORDER_TYPE_BUY) // Grid of BUYs
{
distance = (lastTradePrice - currentBid) / PointValue;
if (distance >= GridStepPips)
{
nextTradeType = ORDER_TYPE_BUY;
nextPrice = currentAsk;
}
}
else if (firstTradeType == ORDER_TYPE_SELL) // Grid of SELLs
{
distance = (currentAsk - lastTradePrice) / PointValue;
if (distance >= GridStepPips)
{
nextTradeType = ORDER_TYPE_SELL;
nextPrice = currentBid;
}
}
if (nextTradeType != WRONG_VALUE)
{
double nextLot = CalculateNextLotSize(openPositionsCount);
if (CheckMarginSufficiency(nextTradeType, nextLot, nextPrice))
{
Print("Opening new grid level ", openPositionsCount + 1, ". Lot: ", nextLot, ". Price: ", nextPrice);
OpenTrade(nextTradeType, nextLot, nextPrice, TakeProfitPips, StopLossPips, MagicNumber);
}
else
{
Print("Not enough margin for new grid level ", openPositionsCount + 1, " (", nextLot, " lots).");
}
}
}
delete positions;
}
//+------------------------------------------------------------------+
//| Helper function: Get open positions for a specific symbol and magic number |
//+------------------------------------------------------------------+
CArrayObj* GetOpenPositions(string symbol, long magic)
{
CArrayObj* positions = new CArrayObj();
if (positions == NULL) return NULL;
for (int i = PositionsTotal() - 1; i >= 0; i--)
{
if (PositionSelectByIndex(i))
{
if (PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_MAGIC) == magic)
{
CPositionInfo* pos = new CPositionInfo();
if (pos != NULL && pos.SelectByIndex(i))
{
positions.Add(pos);
}
else if (pos != NULL)
{
delete pos;
}
}
}
}
// Sort positions by open time (oldest first)
positions.Sort(SORT_BY_OPEN_TIME);
return positions;
}
//+------------------------------------------------------------------+
//| Helper function: Get price of the last opened position |
//+------------------------------------------------------------------+
double GetLastPositionPrice(string symbol, long magic)
{
double lastPrice = 0.0;
datetime lastTime = 0;
for (int i = PositionsTotal() - 1; i >= 0; i--)
{
if (PositionSelectByIndex(i))
{
if (PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_MAGIC) == magic)
{
if (PositionGetInteger(POSITION_TIME) > lastTime)
{
lastTime = PositionGetInteger(POSITION_TIME);
lastPrice = PositionGetDouble(POSITION_PRICE_OPEN);
}
}
}
}
return lastPrice;
}
//+------------------------------------------------------------------+
//| Helper function: Calculate next lot size using Martingale factor |
//+------------------------------------------------------------------+
double CalculateNextLotSize(int currentGridLevel)
{
double nextLot = InitialLot * pow(MartingaleFactor, currentGridLevel);
// Ensure lot size is within min/max limits and step
double minLot = SymbolInfoDouble(SymbolName, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(SymbolName, SYMBOL_VOLUME_MAX);
double lotStep = SymbolInfoDouble(SymbolName, SYMBOL_VOLUME_STEP);
nextLot = fmax(minLot, nextLot);
nextLot = fmin(maxLot, nextLot);
nextLot = NormalizeDouble(round(nextLot / lotStep) * lotStep, 2); // Normalize to lot step
return nextLot;
}
//+------------------------------------------------------------------+
//| CRITICAL: Helper function to check for sufficient free margin |
//+------------------------------------------------------------------+
bool CheckMarginSufficiency(ENUM_ORDER_TYPE type, double lot, double price)
{
double requiredMargin = 0.0;
if (!OrderCalcMargin(type, SymbolName, lot, price, requiredMargin))
{
Print("Error calculating margin for ", EnumToString(type), " ", lot, " lots at ", price, ". Error: ", GetLastError());
return false;
}
double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
if (requiredMargin > freeMargin)
{
Print("INSUFFICIENT MARGIN: Required ", requiredMargin, " for ", EnumToString(type), " ", lot, " lots. Available free margin: ", freeMargin);
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Helper function: Open a trade |
//+------------------------------------------------------------------+
void OpenTrade(ENUM_ORDER_TYPE type, double lot, double price, double tpPips, double slPips, long magic)
{
MqlTradeRequest request;
MqlTradeResult result;
ZeroMemory(request);
request.action = TRADE_ACTION_DEAL;
request.symbol = SymbolName;
request.volume = lot;
request.type = type;
request.price = price;
request.deviation = Slippage * PointValue / _Point; // Convert pips to points for deviation
request.type_filling = ORDER_FILLING_FOK; // Fill or Kill
request.magic = magic;
// Calculate TP/SL prices
if (tpPips > 0)
{
request.tp = (type == ORDER_TYPE_BUY) ? price + tpPips * PointValue : price - tpPips * PointValue;
request.tp = NormalizeDouble(request.tp, DigitsCount);
}
if (slPips > 0)
{
request.sl = (type == ORDER_TYPE_BUY) ? price - slPips * PointValue : price + slPips * PointValue;
request.sl = NormalizeDouble(request.sl, DigitsCount);
}
if (!OrderSend(request, result))
{
Print("OrderSend failed for ", EnumToString(type), " ", lot, " lots. Error: ", GetLastError());
}
else
{
if (result.retcode == TRADE_RETCODE_DONE)
{
Print("Order ", result.order, " (", EnumToString(type), ") opened successfully. Lot: ", lot, ", Price: ", result.price);
}
else
{
Print("OrderSend returned ", result.retcode, ". Comment: ", result.comment);
}
}
}
//+------------------------------------------------------------------+
//| Helper function: Close all positions for a specific symbol and magic number |
//+------------------------------------------------------------------+
void CloseAllPositions(string symbol, long magic)
{
MqlTradeRequest request;
MqlTradeResult result;
for (int i = PositionsTotal() - 1; i >= 0; i--)
{
if (PositionSelectByIndex(i))
{
if (PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_MAGIC) == magic)
{
ZeroMemory(request);
request.action = TRADE_ACTION_DEAL;
request.position = PositionGetInteger(POSITION_TICKET);
request.symbol = PositionGetString(POSITION_SYMBOL);
request.volume = PositionGetDouble(POSITION_VOLUME);
request.magic = PositionGetInteger(POSITION_MAGIC);
ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
if (posType == POSITION_TYPE_BUY)
{
request.type = ORDER_TYPE_SELL;
request.price = SymbolInfoDouble(request.symbol, SYMBOL_BID);
}
else if (posType == POSITION_TYPE_SELL)
{
request.type = ORDER_TYPE_BUY;
request.price = SymbolInfoDouble(request.symbol, SYMBOL_ASK);
}
else
{
Print("Unknown position type: ", posType);
continue;
}
if (!OrderSend(request, result))
{
Print("Error closing position ", request.position, ". Error: ", GetLastError());
}
else
{
if (result.retcode == TRADE_RETCODE_DONE)
{
Print("Position ", request.position, " closed successfully.");
}
else
{
Print("Close position returned ", result.retcode, ". Comment: ", result.comment);
}
}
}
}
}
}
//+------------------------------------------------------------------+
//| Custom class for position info (simplification for example) |
//+------------------------------------------------------------------+
class CPositionInfo : public CObject
{
private:
long m_ticket;
string m_symbol;
ENUM_POSITION_TYPE m_type;
double m_volume;
double m_price_open;
datetime m_time_open;
double m_profit;
double m_swap;
double m_commission;
long m_magic;
public:
CPositionInfo() : m_ticket(0), m_symbol(""), m_type(WRONG_VALUE), m_volume(0.0), m_price_open(0.0), m_time_open(0), m_profit(0.0), m_swap(0.0), m_commission(0.0), m_magic(0) {}
~CPositionInfo() {}
bool SelectByIndex(int index)
{
if (!PositionSelectByIndex(index)) return false;
m_ticket = PositionGetInteger(POSITION_TICKET);
m_symbol = PositionGetString(POSITION_SYMBOL);
m_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
m_volume = PositionGetDouble(POSITION_VOLUME);
m_price_open = PositionGetDouble(POSITION_PRICE_OPEN);
m_time_open = PositionGetInteger(POSITION_TIME);
m_profit = PositionGetDouble(POSITION_PROFIT);
m_swap = PositionGetDouble(POSITION_SWAP);
m_commission = PositionGetDouble(POSITION_COMMISSION);
m_magic = PositionGetInteger(POSITION_MAGIC);
return true;
}
long Ticket() const { return m_ticket; }
string Symbol() const { return m_symbol; }
ENUM_POSITION_TYPE Type() const { return m_type; }
double Volume() const { return m_volume; }
double PriceOpen() const { return m_price_open; }
datetime TimeOpen() const { return m_time_open; }
double Profit() const { return m_profit; }
double Swap() const { return m_swap; }
double Commission() const { return m_commission; }
long Magic() const { return m_magic; }
// Comparison for sorting
virtual int Compare(const CObject* obj, const int mode=0) const
{
const CPositionInfo* other = (CPositionInfo*)obj;
if (other == NULL) return 1;
if (m_time_open < other.m_time_open) return -1;
if (m_time_open > other.m_time_open) return 1;
return 0;
}
};
// Custom sort mode for CArrayObj
#define SORT_BY_OPEN_TIME 0
//+------------------------------------------------------------------+
//| Global function to sort CArrayObj by open time |
//+------------------------------------------------------------------+
void CArrayObj::Sort(const int mode=0)
{
if (mode == SORT_BY_OPEN_TIME)
{
// Simple bubble sort for demonstration, for large arrays consider std::sort or similar
for (int i = 0; i < Total() - 1; i++)
{
for (int j = 0; j < Total() - 1 - i; j++)
{
CPositionInfo* pos1 = (CPositionInfo*)At(j);
CPositionInfo* pos2 = (CPositionInfo*)At(j + 1);
if (pos1 != NULL && pos2 != NULL && pos1.TimeOpen() > pos2.TimeOpen())
{
Exchange(j, j + 1);
}
}
}
}
}
Разбор параметров
Каждый параметр экспертного советника играет ключевую роль в его поведении и управлении рисками. Внимательно изучите их перед использованием.
InitialLot: Начальный объем лота для первой позиции в сетке. Это базовый размер, от которого будут рассчитываться последующие лоты с учетом коэффициента Мартингейла.GridStepPips: Шаг сетки в пунктах. Определяет ценовой интервал, через который будет открываться новая позиция, если цена движется против текущих открытых позиций.MartingaleFactor: Коэффициент Мартингейла. Значение, на которое умножается объем предыдущего лота для определения объема следующей позиции. Например,1.5означает увеличение лота в 1.5 раза. Значение должно быть больше1.0.TakeProfitPips: Общий тейк-профит для всей сетки в пунктах. Когда суммарная прибыль всех открытых позиций достигнет этого значения, все позиции будут закрыты.StopLossPips: Стоп-лосс для первой позиции в пунктах. Если установлено0.0, стоп-лосс не устанавливается. Для последующих позиций стоп-лосс не применяется в данной реализации, так как стратегия Мартингейла обычно не использует индивидуальные стоп-лоссы для каждой позиции в сетке.MaxGridLevels: Максимальное количество уровней сетки. Ограничивает число позиций, которые могут быть открыты в одной сетке. Это критически важный параметр для управления риском, предотвращающий бесконечное наращивание лотов.MagicNumber: Магический номер. Уникальный идентификатор для ордеров, открытых данным экспертным советником. Позволяет EA управлять только своими позициями, не затрагивая другие сделки на счете.Timeframe: Таймфрейм, на котором работает экспертный советник. В данной реализации используетсяPERIOD_CURRENT, что означает работу на текущем графике.Slippage: Допустимое проскальзывание в пунктах при открытии или закрытии ордеров.EnableBuyTrades: Флаг, разрешающий открытие позиций на покупку.EnableSellTrades: Флаг, разрешающий открытие позиций на продажу.
Ключевые функции и их назначение
Рассмотрим основные компоненты нашего экспертного советника:
OnInit(): Инициализация эксперта. Здесь происходит получение свойств символа и базовая проверка входных параметров.OnTick(): Основная логика эксперта, выполняющаяся при каждом новом тике. Она проверяет наличие открытых позиций, рассчитывает общую прибыль, определяет необходимость открытия нового уровня сетки и, самое главное, вызывает функцию проверки маржи.GetOpenPositions(string symbol, long magic): Вспомогательная функция для получения всех открытых позиций, принадлежащих данному символу и магическому номеру. Возвращает массив объектовCPositionInfo, отсортированных по времени открытия.GetLastPositionPrice(string symbol, long magic): Определяет цену открытия самой последней позиции в сетке.CalculateNextLotSize(int currentGridLevel): Вычисляет объем лота для следующей позиции в сетке, применяя коэффициент Мартингейла кInitialLot. Также учитывает минимальный, максимальный лот и шаг лота, установленные брокером.CheckMarginSufficiency(ENUM_ORDER_TYPE type, double lot, double price): Критически важная функция. Она используетOrderCalcMargin()для определения необходимой маржи для потенциальной новой сделки и сравнивает ее с текущей свободной маржой на счете (AccountInfoDouble(ACCOUNT_MARGIN_FREE)). Если свободной маржи недостаточно, функция возвращаетfalse, предотвращая открытие позиции.OpenTrade(ENUM_ORDER_TYPE type, double lot, double price, double tpPips, double slPips, long magic): Функция для отправки торгового запроса на открытие позиции. Устанавливает тейк-профит и стоп-лосс (если заданы) и обрабатывает результат выполнения ордера.CloseAllPositions(string symbol, long magic): Закрывает все открытые позиции, принадлежащие данному эксперту, обычно вызывается при достижении общего тейк-профита.CPositionInfo: Вспомогательный класс для удобного хранения информации об открытых позициях. Включает методCompareдля сортировки по времени открытия.
Как запустить
Для запуска экспертного советника в MetaTrader 5 выполните следующие шаги:
- Сохраните код: Скопируйте предоставленный выше код и сохраните его в файл с расширением
.mq5(например,MartingaleGridEA.mq5) в папкеMQL5/Experts/вашего терминала MetaTrader 5. Вы можете открыть эту папку черезФайл -> Открыть каталог данных -> MQL5 -> Experts. - Скомпилируйте: Откройте MetaEditor (нажмите
F4в MetaTrader 5), найдите ваш файл в навигаторе проекта и нажмитеКомпилировать(илиF7). Убедитесь, что нет ошибок компиляции. - Прикрепите к графику: Перетащите
MartingaleGridEAиз окнаНавигатор(разделСоветники) на график нужной валютной пары (например, EURUSD). - Настройте параметры: Во вкладке
Общиеустановите флажокРазрешить алгоритмическую торговлю. Во вкладкеВходные параметрынастройте значения, описанные выше, в соответствии с вашей стратегией и допустимым риском. - Включите алгоритмическую торговлю: Убедитесь, что кнопка
Алгоритмическая торговляна панели инструментов MetaTrader 5 активна (зеленая).
Помните, что стратегии Мартингейла сопряжены с высокими рисками. Всегда тестируйте экспертов на демо-счете перед использованием на реальном. Для более глубокого анализа торговых систем и портфелей, вы можете изучить такие инструменты, как Кастомный индикатор Equity Curve для мультивалютного портфеля. Также полезно понимать рыночные циклы, используя, например, Индикатор профиля рынка (Market Profile) на MQL5: гистограмма TPO, или анализировать данные COT, как описано в статье Парсинг отчетов COT с CFTC и индикатор нетто-позиций для MT4.
Заключение
Реализация сетки ордеров с Мартингейлом и проверкой маржи в MQL5 — это мощный инструмент, который, при правильной настройке и понимании рисков, может быть эффективным. Встроенная проверка маржи является критически важным элементом безопасности, предотвращающим неконтролируемое увеличение позиций и потенциальный стоп-аут. Всегда подходите к таким стратегиям с осторожностью и используйте адекватное управление капиталом.




