MQL5: Скрипт для сбора статистики проскальзываний Market ордеров

MQL5: Скрипт для сбора статистики проскальзываний Market ордеров MQL4 / MQL5 (MetaTrader)
Создайте MQL5 советник для автоматического сбора статистики проскальзываний (slippage) при исполнении Market ордеров. Анализируйте разницу между запрошенной и исполненной ценой, логируйте данные в CSV для глубокого анализа.
Суть: Мы разработаем MQL5 эксперт-советник, который будет периодически отправлять Market ордера (BUY/SELL) на заданном символе, немедленно их закрывать и фиксировать разницу между запрошенной и фактически исполненной ценой (проскальзывание) в CSV-файл. Это позволит собрать статистику проскальзываний для дальнейшего анализа.

Проскальзывание (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 вашего терминала.

Как запустить

Для запуска советника и начала сбора статистики проскальзываний выполните следующие шаги:

  1. Откройте MetaEditor: В MetaTrader 5 нажмите F4 или выберите Инструменты -> MetaQuotes Language Editor.

  2. Создайте новый эксперт: В MetaEditor выберите Файл -> Создать -> Эксперт (шаблон). Назовите его, например, SlippageCollectorEA.

  3. Вставьте код: Удалите весь сгенерированный код и вставьте предоставленный выше исходный код в файл SlippageCollectorEA.mq5.

  4. Скомпилируйте: Нажмите кнопку Компилировать (или F7). Убедитесь, что нет ошибок компиляции. Если есть, проверьте, что вы скопировали код полностью и без искажений.

  5. Перезапустите MetaTrader 5: Иногда это необходимо для того, чтобы новый советник появился в списке.

  6. Прикрепите советник к графику: В MetaTrader 5 откройте график того символа, для которого вы хотите собирать статистику (или любого другого, если вы указали SymbolToTrade). Перетащите SlippageCollectorEA из окна Навигатор (раздел Советники) на график.

  7. Настройте параметры: В появившемся окне свойств советника перейдите на вкладку Входные параметры и настройте значения SymbolToTrade, Volume, SlippagePoints, TradeIntervalSeconds, MagicNumber и LogFileName по своему усмотрению. Убедитесь, что на вкладке Общие установлен флажок Разрешить автоматическую торговлю.

  8. Запустите: Нажмите ОК. На графике в правом верхнем углу должен появиться улыбающийся смайлик, а в журнале экспертов (вкладка Эксперты в окне Терминал) вы увидите сообщения о работе советника.

  9. Просмотр лога: Файл SlippageLog.csv будет создан в папке MQL5/Files. Вы можете открыть эту папку, выбрав Файл -> Открыть каталог данных в MetaTrader 5, затем перейдя в MQL5 -> Files. Откройте CSV-файл в любом табличном редакторе (Excel, Google Sheets) или используйте его для анализа в Python/R.

Помните, что советник будет открывать и закрывать реальные сделки на вашем счете. Используйте его на демо-счете или с минимальным объемом на реальном счете, чтобы избежать нежелательных финансовых рисков. Успешного сбора статистики!

Оцените статью
FinFluct