Проскальзывание (slippage) — это неизбежная реальность при исполнении рыночных ордеров, особенно в условиях высокой волатильности или низкой ликвидности. Для количественных трейдеров понимание и измерение проскальзывания критически важно, поскольку оно напрямую влияет на прибыльность торговых стратегий. В этой статье мы, как Senior Quant Developer, представим готовый MQL5 скрипт-советник, который автоматизирует сбор этих ценных данных.
Наш советник будет работать в фоновом режиме, периодически отправляя тестовые Market ордера, фиксируя запрошенную цену и фактическую цену исполнения, а затем немедленно закрывая позицию, чтобы избежать накопления реальных сделок. Все данные будут аккуратно записываться в CSV-файл для последующего анализа в Excel, Python или R.
Этот инструмент станет незаменимым помощником для оценки качества исполнения ордеров у вашего брокера, сравнения условий на разных символах или в разное время суток. Он дополняет другие наши разработки, такие как скрипт для массового закрытия позиций или автоматическое выравнивание объемов ордеров, предоставляя еще один слой контроля над торговым процессом.
Исходный код
Представленный ниже MQL5 код реализует эксперт-советник SlippageCollectorEA. Он использует библиотеку CTrade для отправки и закрытия ордеров, а также функции работы с файлами для записи статистики. Советник отправляет Market ордера BUY или SELL с заданным интервалом, фиксирует цены, рассчитывает проскальзывание в пунктах, пипсах и долларах США, а затем записывает эти данные в CSV-файл. После исполнения ордера позиция немедленно закрывается.
//+------------------------------------------------------------------+
//| SlippageCollectorEA.mq5 |
//| Copyright 2024, FinFluct.com |
//| |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, FinFluct.com"
#property link "https://finfluct.com"
#property version "1.00"
#property description "Эксперт-советник для сбора статистики проскальзываний Market ордеров."
#property description "Открывает и немедленно закрывает ордера для измерения Slippage."
#include
#include // Для работы с файлами
//--- Входные параметры
input string SymbolToTrade = "EURUSD"; // Символ для тестирования (если отличается от текущего)
input double Volume = 0.1; // Объем ордера для тестирования
input int SlippagePoints = 50; // Максимальное проскальзывание в пунктах (для OrderSend)
input int TradeIntervalSeconds = 10; // Интервал между тестовыми ордерами (секунды)
input ulong MagicNumber = 12345; // Магический номер для ордеров
input string LogFileName = "SlippageLog.csv"; // Имя файла для логирования
//--- Глобальные переменные
CTrade m_trade;
datetime m_lastTradeTime;
int m_fileHandle = INVALID_HANDLE;
string m_logFilePath;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
//--- Инициализация объекта CTrade
m_trade.SetAsyncMode(false); // Синхронный режим для надежного получения результатов
m_trade.SetExpertMagic(MagicNumber);
//--- Формирование полного пути к файлу лога (для информации)
m_logFilePath = TerminalInfoString(TERMINAL_DATA_PATH) + "\\MQL5\\Files\\" + LogFileName;
//--- Открытие файла для записи (или создание, если не существует)
// FileOpen открывает файл относительно папки MQL5/Files
m_fileHandle = FileOpen(LogFileName, FILE_READ | FILE_WRITE | FILE_CSV | FILE_SHARE_READ, ',');
if (m_fileHandle == INVALID_HANDLE)
{
Print("Ошибка: Не удалось открыть файл лога ", LogFileName, ". Ошибка: ", GetLastError());
return INIT_FAILED;
}
//--- Проверка, пуст ли файл, и запись заголовка, если это новый файл
if (FileSize(m_fileHandle) == 0)
{
FileWriteString(m_fileHandle, "Timestamp,Symbol,OrderType,RequestedPrice,ExecutedPrice,SlippagePips,SlippagePoints,SlippageUSD\n");
FileFlush(m_fileHandle);
}
m_lastTradeTime = TimeCurrent();
Print("SlippageCollectorEA инициализирован. Логирование в: ", m_logFilePath);
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
//--- Закрытие файла лога
if (m_fileHandle != INVALID_HANDLE)
{
FileClose(m_fileHandle);
Print("Файл лога ", LogFileName, " закрыт.");
}
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
//--- Проверка интервала между сделками
if (TimeCurrent() - m_lastTradeTime < TradeIntervalSeconds)
{
return;
}
//--- Обновление времени последней сделки
m_lastTradeTime = TimeCurrent();
//--- Получение текущих цен
MqlTick tick;
if (!SymbolInfoTick(SymbolToTrade, tick))
{
Print("Ошибка получения тика для символа ", SymbolToTrade, ". Ошибка: ", GetLastError());
return;
}
double bid = tick.bid;
double ask = tick.ask;
//--- Случайный выбор типа ордера (BUY или SELL)
ENUM_ORDER_TYPE orderType;
if (MathRand() % 2 == 0)
{
orderType = ORDER_TYPE_BUY;
}
else
{
orderType = ORDER_TYPE_SELL;
}
MqlTradeRequest request;
MqlTradeResult result;
ZeroMemory(request);
ZeroMemory(result);
request.action = TRADE_ACTION_DEAL;
request.symbol = SymbolToTrade;
request.volume = Volume;
request.type = orderType;
request.type_filling = ORDER_FILLING_FOK; // Fill or Kill для Market ордеров
request.deviation = SlippagePoints; // Максимальное отклонение в пунктах
request.magic = MagicNumber;
double requestedPrice = 0;
if (orderType == ORDER_TYPE_BUY)
{
requestedPrice = ask;
request.price = requestedPrice; // Запрашиваемая цена
if (!m_trade.Buy(Volume, SymbolToTrade, requestedPrice, 0, 0, "Slippage Test BUY"))
{
Print("Ошибка отправки BUY ордера: ", m_trade.ResultRetcode(), " - ", m_trade.ResultComment());
return;
}
}
else // ORDER_TYPE_SELL
{
requestedPrice = bid;
request.price = requestedPrice; // Запрашиваемая цена
if (!m_trade.Sell(Volume, SymbolToTrade, requestedPrice, 0, 0, "Slippage Test SELL"))
{
Print("Ошибка отправки SELL ордера: ", m_trade.ResultRetcode(), " - ", m_trade.ResultComment());
return;
}
}
//--- Получение результата сделки
// CTrade автоматически заполняет m_trade.Result* после успешной операции
if (m_trade.ResultRetcode() == TRADE_RETCODE_DONE)
{
double executedPrice = m_trade.ResultDealPrice();
ulong dealTicket = m_trade.ResultDeal();
//--- Расчет проскальзывания
double slippagePoints = GetSlippageInPoints(requestedPrice, executedPrice, orderType);
double slippagePips = GetSlippageInPips(requestedPrice, executedPrice, orderType);
double slippageUSD = GetSlippageInUSD(requestedPrice, executedPrice, orderType, Volume);
PrintFormat("Deal #%I64u: %s %s %f @ %.*f (Requested: %.*f). Slippage: %.2f Pips (%.0f Points, $%.2f)",
dealTicket, EnumToString(orderType), SymbolToTrade,
(int)SymbolInfoInteger(SymbolToTrade, SYMBOL_DIGITS), executedPrice,
(int)SymbolInfoInteger(SymbolToTrade, SYMBOL_DIGITS), requestedPrice,
slippagePips, slippagePoints, slippageUSD);
//--- Логирование
LogSlippage(TimeCurrent(), SymbolToTrade, orderType, requestedPrice, executedPrice, slippagePips, slippagePoints, slippageUSD);
//--- Немедленное закрытие позиции, чтобы не накапливать
// Находим позицию по MagicNumber и символу
PositionSelect(SymbolToTrade);
if (PositionGetInteger(POSITION_MAGIC) == MagicNumber)
{
if (!m_trade.PositionClose(SymbolToTrade, ASYNC_MODE_AUTOMATIC)) // ASYNC_MODE_AUTOMATIC для CTrade
{
Print("Ошибка закрытия позиции ", SymbolToTrade, ": ", m_trade.ResultRetcode(), " - ", m_trade.ResultComment());
}
}
}
else
{
Print("Ордер не был исполнен. Код ошибки: ", m_trade.ResultRetcode(), " - ", m_trade.ResultComment());
}
}
//+------------------------------------------------------------------+
//| Helper: Calculates slippage in points |
//+------------------------------------------------------------------+
double GetSlippageInPoints(double requestedPrice, double executedPrice, ENUM_ORDER_TYPE orderType)
{
double point = SymbolInfoDouble(SymbolToTrade, SYMBOL_POINT);
if (point == 0) return 0; // Избегаем деления на ноль
if (orderType == ORDER_TYPE_BUY)
{
return (executedPrice - requestedPrice) / point;
}
else // ORDER_TYPE_SELL
{
return (requestedPrice - executedPrice) / point;
}
}
//+------------------------------------------------------------------+
//| Helper: Calculates slippage in pips |
//+------------------------------------------------------------------+
double GetSlippageInPips(double requestedPrice, double executedPrice, ENUM_ORDER_TYPE orderType)
{
double point = SymbolInfoDouble(SymbolToTrade, SYMBOL_POINT);
if (point == 0) return 0; // Избегаем деления на ноль
// Стандартное определение пипса (обычно 10 пунктов для 5-значных символов)
double pipSize = point * 10;
if (orderType == ORDER_TYPE_BUY)
{
return (executedPrice - requestedPrice) / pipSize;
}
else // ORDER_TYPE_SELL
{
return (requestedPrice - executedPrice) / pipSize;
}
}
//+------------------------------------------------------------------+
//| Helper: Calculates slippage in USD |
//+------------------------------------------------------------------+
double GetSlippageInUSD(double requestedPrice, double executedPrice, ENUM_ORDER_TYPE orderType, double volume)
{
double point = SymbolInfoDouble(SymbolToTrade, SYMBOL_POINT);
if (point == 0) return 0; // Избегаем деления на ноль
// Значение одного пункта для одного лота в валюте депозита
// SYMBOL_TRADE_TICK_VALUE - это стоимость SYMBOL_TRADE_TICK_SIZE изменения цены для 1 лота
// Поэтому (SYMBOL_TRADE_TICK_VALUE / SYMBOL_TRADE_TICK_SIZE) дает стоимость единицы изменения цены для 1 лота
// Умножаем на 'point', чтобы получить стоимость одного пункта для 1 лота
double valuePerPointPerLot = SymbolInfoDouble(SymbolToTrade, SYMBOL_TRADE_TICK_VALUE) / SymbolInfoDouble(SymbolToTrade, SYMBOL_TRADE_TICK_SIZE) * point;
double slippagePoints = GetSlippageInPoints(requestedPrice, executedPrice, orderType);
return slippagePoints * valuePerPointPerLot * volume;
}
//+------------------------------------------------------------------+
//| Helper: Logs slippage data to CSV |
//+------------------------------------------------------------------+
void LogSlippage(datetime timestamp, string symbol, ENUM_ORDER_TYPE orderType, double requestedPrice, double executedPrice, double slippagePips, double slippagePoints, double slippageUSD)
{
if (m_fileHandle == INVALID_HANDLE)
{
Print("Ошибка: Файл лога не открыт.");
return;
}
string line = StringFormat("%s,%s,%s,%.*f,%.*f,%.2f,%.0f,%.2f\n",
TimeToString(timestamp, TIME_DATE | TIME_SECONDS),
symbol,
EnumToString(orderType),
(int)SymbolInfoInteger(symbol, SYMBOL_DIGITS), requestedPrice,
(int)SymbolInfoInteger(symbol, SYMBOL_DIGITS), executedPrice,
slippagePips,
slippagePoints,
slippageUSD);
FileWriteString(m_fileHandle, line);
FileFlush(m_fileHandle); // Сброс буфера на диск для немедленной записи
}
Разбор параметров
Советник имеет следующие входные параметры, которые можно настроить через окно свойств эксперта в MetaTrader 5:
SymbolToTrade: Символ, на котором будет производиться сбор статистики проскальзываний. По умолчанию"EURUSD". Вы можете указать любой другой доступный символ.Volume: Объем ордера (в лотах), который будет использоваться для тестовых сделок. Рекомендуется использовать минимально допустимый объем (например, 0.01 или 0.1), чтобы минимизировать риски.SlippagePoints: Максимальное допустимое проскальзывание в пунктах, которое вы указываете при отправке ордера. Если фактическое проскальзывание превысит это значение, ордер может быть отклонен. Это параметр для функцииOrderSend, а не для измерения.TradeIntervalSeconds: Интервал в секундах между отправкой тестовых ордеров. Например, 10 секунд означает, что новый ордер будет отправляться каждые 10 секунд.MagicNumber: Уникальный магический номер, который будет присваиваться всем ордерам, открытым этим советником. Это позволяет советнику идентифицировать и управлять только своими позициями, не затрагивая другие сделки на счете. Это особенно важно, если вы используете другие советники, например, для расчета маржинальных требований.LogFileName: Имя CSV-файла, в который будут записываться данные о проскальзываниях. Файл будет создан в папкеMQL5/Filesвашего терминала.
Как запустить
Для запуска советника и начала сбора статистики проскальзываний выполните следующие шаги:
-
Откройте MetaEditor: В MetaTrader 5 нажмите
F4или выберитеИнструменты -> MetaQuotes Language Editor. -
Создайте новый эксперт: В MetaEditor выберите
Файл -> Создать -> Эксперт (шаблон). Назовите его, например,SlippageCollectorEA. -
Вставьте код: Удалите весь сгенерированный код и вставьте предоставленный выше исходный код в файл
SlippageCollectorEA.mq5. -
Скомпилируйте: Нажмите кнопку
Компилировать(илиF7). Убедитесь, что нет ошибок компиляции. Если есть, проверьте, что вы скопировали код полностью и без искажений. -
Перезапустите MetaTrader 5: Иногда это необходимо для того, чтобы новый советник появился в списке.
-
Прикрепите советник к графику: В MetaTrader 5 откройте график того символа, для которого вы хотите собирать статистику (или любого другого, если вы указали
SymbolToTrade). ПеретащитеSlippageCollectorEAиз окнаНавигатор(разделСоветники) на график. -
Настройте параметры: В появившемся окне свойств советника перейдите на вкладку
Входные параметрыи настройте значенияSymbolToTrade,Volume,SlippagePoints,TradeIntervalSeconds,MagicNumberиLogFileNameпо своему усмотрению. Убедитесь, что на вкладкеОбщиеустановлен флажокРазрешить автоматическую торговлю. -
Запустите: Нажмите
ОК. На графике в правом верхнем углу должен появиться улыбающийся смайлик, а в журнале экспертов (вкладкаЭкспертыв окнеТерминал) вы увидите сообщения о работе советника. -
Просмотр лога: Файл
SlippageLog.csvбудет создан в папкеMQL5/Files. Вы можете открыть эту папку, выбравФайл -> Открыть каталог данныхв MetaTrader 5, затем перейдя вMQL5 -> Files. Откройте CSV-файл в любом табличном редакторе (Excel, Google Sheets) или используйте его для анализа в Python/R.
Помните, что советник будет открывать и закрывать реальные сделки на вашем счете. Используйте его на демо-счете или с минимальным объемом на реальном счете, чтобы избежать нежелательных финансовых рисков. Успешного сбора статистики!




