English
preview
Анализ CSV-данных (Часть 6): Нормализация результатов по нескольким брокерам и сверка CSV между платформами

Анализ CSV-данных (Часть 6): Нормализация результатов по нескольким брокерам и сверка CSV между платформами

MetaTrader 5Статистика и анализ |
46 0
Ushana Kevin Iorkumbul
Ushana Kevin Iorkumbul

Введение

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

Если CSV-экспорты разных брокеров без предварительной обработки загрузить в один и тот же конвейер Python, анализ может быть незаметно искажён. Два брокера, использующие разные форматы котирования EURUSD, будут показывать показатели прибыли, различающиеся в десять раз. Если один брокер записывает комиссию в отдельное поле DEAL_COMMISSION, распределение прибыли будет отличаться от результатов брокера, включающего комиссию в спред. Брокер с часовым поясом UTC+2 и брокер с UTC+3 сформируют последовательности временных меток со смещением на один час при совместном отображении, из-за чего временной анализ ошибочно отнесёт сделки к неверным торговым сессиям.

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

Примечание. Если вы пропустили Часть 5 этой серии, её можно прочитать здесь.



Источники расхождений схем: классификация

Соглашения о пунктах и точность котирования

Наиболее распространённый источник числовых расхождений — различие между четырёх- и пятизначным котированием. У четырёхзначного брокера EURUSD котируется с четырьмя знаками после запятой, а один пипс равен 0,0001. У пятизначного брокера та же пара котируется с пятью знаками после запятой: один пункт равен 0,00001, тогда как пипс по-прежнему равен 0,0001. Большинство современных брокеров используют пятизначное котирование, но устаревшие конфигурации и некоторые экзотические пары всё ещё работают в четырёхзначном режиме.

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

Модели учёта комиссий и свопов

У брокеров MetaTrader 5 встречаются три основные модели учёта комиссии:

  • Модель с комиссией в спреде: комиссия брокера включена в спред между ценами Bid и Ask. Поле DEAL_COMMISSION не содержит ненулевого значения. DEAL_PROFIT закрывающей сделки отражает полный чистый финансовый результат.
  • Модель с отдельным начислением комиссии по сделке: брокер взимает фиксированную сумму в долларах за каждый лот с каждой стороны сделки и записывает её в DEAL_COMMISSION открывающей сделки. DEAL_PROFIT закрывающей сделки показывает валовой финансовый результат до комиссии; для получения фактической чистой прибыли комиссию необходимо вычесть.
  • Модель комиссии, зависящей от объёма: ступенчатая тарифная сетка изменяет ставку за лот в зависимости от месячного объёма. Поэтому комиссии по сделкам одинакового размера различаются, и предположение о постоянной ставке становится неверным.

Соглашения об именовании символов

Названия символов не стандартизированы. Один инструмент может встречаться под множеством обозначений:

Инструмент Возможные названия символа
Евро / Доллар США EURUSD, EURUSDm, EURUSD., EURUSD_i, FX_EURUSD
Золото / Доллар США XAUUSD, GOLD, XAUUSDm, GOLDm, XAUUSD.r
Индекс US 30 US30, DJ30, USTEC30, Dow Jones, #DJ30
Нефть WTI USOIL, XTIUSD, WTI, CrudeOil, OIL.WTI

Без нормализации стратегия, протестированная на EURUSD у брокера A и на EURUSDm у брокера B, создаст две отдельные группы в любой агрегации, использующей groupby('Symbol'), что сделает межброкерское сравнение невозможным. Нормализатор должен сопоставлять все варианты названий с канонической формой.

Форматы временных меток и временные смещения

MetaTrader 5 работает по серверному времени подключённого брокера, которое различается между брокерами и может сезонно меняться из-за перехода на летнее время. Распространённые часовые пояса серверов включают UTC+0, UTC+2 (EET/EEST в зависимости от летнего времени), UTC+3 (MSK или EEST в зависимости от настроек брокера) и UTC−5 (EST). Стандартная функция MQL5 TimeToString() формирует строку локального серверного времени без указания часового пояса.

При сравнении экспортов брокеров UTC+2 и UTC+3 по часам суток каждая сделка, записанная между 00:00 и 00:59 в экспорте UTC+2, соответствует сделке между 01:00 и 01:59 в экспорте UTC+3. До начала любого временного анализа нормализация должна преобразовать все временные метки в UTC.

Различия в валюте счёта

Не все брокеры выражают средства счёта и прибыль по сделкам в долларах США. Брокер, ориентированный на европейских клиентов, может вести счета в EUR, а брокер, работающий преимущественно с Великобританией, — в GBP. Если без конвертации объединить экспорты со счетов в разных валютах, прибыль €100 и $100 будет выглядеть как одинаковое числовое значение, хотя фактические результаты различаются. Слой нормализации переводит все показатели прибыли и средств счёта в единую валюту — в данной реализации в USD.


Проектирование контракта нормализации

Каноническая схема

На выходе слой нормализации формирует единый DataFrame с фиксированной канонической схемой. Каждая строка независимо от исходного брокера содержит один и тот же набор столбцов с одинаковым смыслом:

Столбец Тип Смысл после нормализации
Broker_ID string Краткий идентификатор исходного брокера
Symbol_Canonical string Нормализованное имя инструмента (например, EURUSD или XAUUSD)
Timeframe string Обозначение периода графика (без изменений относительно источника)
Test_Phase string Метка фазы: Baseline, InSample, OutSample
Open_Time_UTC datetime Время открытия бара или сделки, приведённое к UTC
Close_Time_UTC datetime Время закрытия бара или сделки, приведённое к UTC
Net_Profit_USD float Прибыль после комиссии, выраженная в USD
Commission_USD float Комиссионные расходы в USD
Gross_Profit_USD float Прибыль до комиссии в USD
Volume_Lots float Объём сделки в стандартных лотах
Slippage_Points float Проскальзывание исполнения в нормализованных пунктах пятизначного котирования
Sortino_Ratio float Без изменений относительно источника
Max_Drawdown_PCT float Без изменений относительно источника
False_Flips_Whipsaws int Без изменений относительно источника
Avg_Lag_On_Turn_Bars float  Без изменений относительно источника

Определения брокерских профилей

Каждый брокер описывается словарём профиля, в котором задаются все параметры среды, необходимые для нормализации:

BROKER_PROFILES = {
    "BrokerA": {
        "display_name"      : "Broker A (ECN)",
        "server_utc_offset" : 2,
        "digit_convention"  : 5,
        "commission_model"  : "per_trade",
        "commission_per_lot": 3.50,
        "account_currency"  : "USD",
        "symbol_map"        : {
            "EURUSD" : "EURUSD", "GBPUSD" : "GBPUSD",
            "XAUUSD" : "XAUUSD", "US30"   : "US30",
        }
    },
    "BrokerB": {
        "display_name"      : "Broker B (Market Maker)",
        "server_utc_offset" : 3,
        "digit_convention"  : 5,
        "commission_model"  : "spread_only",
        "commission_per_lot": 0.0,
        "account_currency"  : "EUR",
        "symbol_map"        : {
            "EURUSDm" : "EURUSD", "GBPUSDm" : "GBPUSD",
            "GOLDm"   : "XAUUSD", "DJ30"    : "US30",
        }
    },
    "BrokerC": {
        "display_name"      : "Broker C (STP)",
        "server_utc_offset" : 0,
        "digit_convention"  : 4,
        "commission_model"  : "per_trade",
        "commission_per_lot": 5.00,
        "account_currency"  : "GBP",
        "symbol_map"        : {
            "EURUSD"   : "EURUSD", "GBPUSD"   : "GBPUSD",
            "XAUUSD"   : "XAUUSD", "Dow Jones": "US30",
        }
    }
}

Эта структура профиля служит единым источником истины для всех параметров, зависящих от брокера. Чтобы добавить нового брокера, достаточно создать новую запись в BROKER_PROFILES — изменять саму логику нормализации не требуется.


Расширение экспорта MQL5 с учётом брокера

Дополнение экспортируемой строки брокерскими метаданными

Существующая схема экспорта из Частей 1–4 не содержит полей, идентифицирующих брокерскую среду. Для нормализации данных от нескольких брокеров советник или индикатор MQL5 должен записывать в экспортируемую строку несколько дополнительных полей:

  • Broker_Server — AccountInfoString(ACCOUNT_SERVER)
  • Account_Currency — AccountInfoString(ACCOUNT_CURRENCY)
  • Symbol_Digits — (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS)
  • Commission_Per_Lot — входная переменная, задаваемая пользователем
  • Slippage_Points — измеренная разница между запрошенной ценой и ценой исполнения, выраженная в пунктах

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

Реализация BrokerAwareExporter.mqh

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

//+------------------------------------------------------------------+
//|                                      BrokerAwareExporter.mqh     |
//|   Extends the base CSV export schema with broker metadata fields |
//+------------------------------------------------------------------+

#property strict

#ifndef BROKER_AWARE_EXPORTER_MQH
#define BROKER_AWARE_EXPORTER_MQH

//+------------------------------------------------------------------+
//| Extended metrics struct: adds broker environment fields          |
//+------------------------------------------------------------------+
struct SBrokerAwareMetrics
  {
   //--- Standard performance fields (carried from prior articles)
   string            testPhase;           // Evaluation phase label
   string            indicatorName;       // Base technical filter label
   int               filterPeriod;        // Primary indicator lookback window
   double            netProfit;           // Total financial return metrics
   double            sortinoRatio;        // Downside risk-adjusted return ratio
   double            maxDrawdownPct;      // Peak-to-trough draw percentage
   int               falseFlipsWhipsaws;  // Total count of bad pivot entry breaks
   double            avgLagBars;          // Mean signal structural latency bars
   int               totalTrades;         // Cumulative closed position counts

   //--- Broker environment fields (new in this article)
   string            brokerServer;        // AccountInfoString(ACCOUNT_SERVER)
   string            accountCurrency;     // AccountInfoString(ACCOUNT_CURRENCY)
   int               symbolDigits;        // SYMBOL_DIGITS for the tested instrument
   double            commissionPerLot;    // User-specified commission rate (input)
   double            totalCommissionPaid; // Summed DEAL_COMMISSION across all deals
   double            avgSlippagePoints;   // Mean abs(requested − filled) in points.
   double            grossProfit;         // Net profit before commission deduction
  };

В основной программе доступны два входных параметра:

//--- Input parameters for broker alignment checks
input group "--- Broker Metadata ---";
input double InpCommissionPerLot = 3.50;  // Commission per lot per side (USD)
input long   InpMagicNumber      = 12345; // Magic number to filter own deals

Функция PopulateBrokerFields() считывает свойства счёта и символа. Затем она просматривает историю сделок, отбирает операции с магическим номером советника, суммирует комиссию и приближённо оценивает проскальзывание по величине спреда, если брокер использует плавающий спред. Константа SYMBOL_SPREAD_FLOAT позволяет определить, является спред фиксированным или плавающим:

//+------------------------------------------------------------------+
//| Populates broker environment fields from live terminal context   |
//+------------------------------------------------------------------+
void PopulateBrokerFields(SBrokerAwareMetrics &m)
  {
   m.brokerServer     = AccountInfoString(ACCOUNT_SERVER);
   m.accountCurrency  = AccountInfoString(ACCOUNT_CURRENCY);
   m.symbolDigits     = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   m.commissionPerLot = InpCommissionPerLot;

//--- Accumulate slippage and commission from deal history
   double sum_slippage   = 0.0;
   double sum_commission = 0.0;
   int    slippage_count = 0;

   HistorySelect(0, TimeCurrent());
   int total_deals = HistoryDealsTotal();

   for(int i = 0; i < total_deals; i++)
     {
      ulong ticket = HistoryDealGetTicket(i);
      if(ticket == 0)
         continue;

      //--- Only process deals placed by this EA
      if(HistoryDealGetInteger(ticket, DEAL_MAGIC) != InpMagicNumber)
         continue;

      //--- Accumulate commission from all deal types
      sum_commission += MathAbs(HistoryDealGetDouble(ticket, DEAL_COMMISSION));

      //--- Compute slippage on entry deals: estimated from average spread
      if(HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_IN)
        {
         double fill_price = HistoryDealGetDouble(ticket, DEAL_PRICE);
         double point_size = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
         if(point_size > 0.0 && fill_price > 0.0)
           {
            //--- Check if the broker uses floating spread
            double spread_pts = (SymbolInfoInteger(_Symbol, SYMBOL_SPREAD_FLOAT) != 0)
                                ? (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD)
                                : 0.0;
            sum_slippage += spread_pts;
            slippage_count++;
           }
        }
     }

   m.totalCommissionPaid = sum_commission;
   m.avgSlippagePoints   = (slippage_count > 0) ? (sum_slippage / slippage_count) : 0.0;
   m.grossProfit         = m.netProfit + sum_commission;
  }

Функция ExportBrokerAwareRow() координирует экспорт. Она вызывает PopulateBrokerFields(), задаёт флаги файла, записывает заголовок в новый файл и добавляет одну форматированную строку CSV с использованием механизма активного ожидания (spin-lock) из Части 1

//+------------------------------------------------------------------+
//| ExportBrokerAwareRow: the single public export call              |
//+------------------------------------------------------------------+
void ExportBrokerAwareRow(SBrokerAwareMetrics &metrics, const string file_name, const bool use_common = true)
  {
   string tf_str = EnumToString(_Period);
   StringReplace(tf_str, "PERIOD_", "");

   PopulateBrokerFields(metrics);

//--- Build access flags
   int flags = FILE_CSV | FILE_ANSI | FILE_WRITE | FILE_READ;
   if(use_common)
      flags |= FILE_COMMON;

//--- Write header if file is new
   bool file_exists = FileIsExist(file_name, use_common ? FILE_COMMON : 0);

//--- Spin-lock open (reuses pattern from Part 1)
   int file_handle = INVALID_HANDLE;
   for(int attempt = 0; attempt < 50000; attempt++)
     {
      file_handle = FileOpen(file_name, flags, ',');
      if(file_handle != INVALID_HANDLE)
         break;
     }

   if(file_handle == INVALID_HANDLE)
     {
      PrintFormat("[BrokerAwareExporter] Failed to open [%s]. Error: %d", file_name, GetLastError());
      return;
     }

   FileSeek(file_handle, 0, SEEK_END);

   if(!file_exists)
      FileWriteString(file_handle, BuildBrokerAwareHeader() + "\n");

   FileWriteString(file_handle, BuildBrokerAwareRow(metrics, tf_str) + "\n");
   FileClose(file_handle);
  }

Проверка экспорта с помощью минимального советника

Чтобы убедиться, что подключаемый файл компилируется и работает, используется минимальный советник Test_BrokerAware_EA.mq5. На первом тике он заполняет тестовую структуру SBrokerAwareMetrics и вызывает ExportBrokerAwareRow():

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Export only once to avoid flooding the file system
   if(g_exported)
      return;
   g_exported = true;

//--- Prepare a sample metrics struct
   SBrokerAwareMetrics m;

//--- Populate standard performance fields (dummy data for testing)
   m.testPhase          = "TestRun";
   m.indicatorName      = "EMA_Crossover";
   m.filterPeriod       = 14;
   m.netProfit          = 500.0;
   m.sortinoRatio       = 1.5;
   m.maxDrawdownPct     = 12.0;
   m.falseFlipsWhipsaws = 5;
   m.avgLagBars         = 2.3;
   m.totalTrades        = 10;

//--- Broker fields will be populated automatically by
//--- PopulateBrokerFields() inside ExportBrokerAwareRow().
//--- We initialize them to safe baseline default values here.
   m.brokerServer        = "";
   m.accountCurrency     = "";
   m.symbolDigits        = 0;
   m.commissionPerLot    = InpCommissionPerLot;  // Pulled from underlying inclusion file input
   m.totalCommissionPaid = 0.0;
   m.avgSlippagePoints   = 0.0;
   m.grossProfit         = 0.0;

//--- Export the tracking row out to shared architecture layers
   string output_file = "BrokerAware_Test.csv";
   ExportBrokerAwareRow(m, output_file, true);

   PrintFormat("[Test EA] Row exported to %s. Check MQL5/Files (Common folder).", output_file);
  }

Полученный CSV содержит все 17 столбцов — стандартные показатели эффективности и брокерские метаданные — и позволяет проверить структуру схемы. Ниже приведён пример результата.

Broker-Aware CSV Output

Рис. 1. CSV-экспорт с учётом брокера. Снимок файла BrokerAware_Test.csv, открытого в табличном редакторе.


Слой нормализации Python: broker_normalizer.py

Модуль Python применяет к исходным брокерским экспортам последовательность преобразований и формирует единый канонический DataFrame. Во всех визуализациях используется единая светлая тема графиков.

Загрузка и маркировка исходных брокерских экспортов

def load_broker_export(file_path: str, broker_id: str) -> Optional[pd.DataFrame]:
    if not os.path.isfile(file_path):
        logging.error(f"File not found: {file_path}")
        print(f"[Normalizer] Error: {file_path} not found.")
        return None
    try:
        df = pd.read_csv(file_path, encoding="ansi", low_memory=False)
    except UnicodeDecodeError:
        try:
            df = pd.read_csv(file_path, encoding="utf-8", low_memory=False)
        except Exception as exc:
            logging.error(f"Failed to parse {file_path}: {exc}")
            return None
    df["Broker_ID"] = broker_id
    logging.info(f"Loaded {len(df):,} rows from {os.path.basename(file_path)} [{broker_id}]")
    return df

Функция сначала пытается прочитать файл в кодировке ANSI, при ошибке переключается на UTF-8, а затем добавляет каждой строке столбец Broker_ID.

Нормализация разрядности пунктов

def normalize_pip_convention(df: pd.DataFrame, broker_id: str) -> pd.DataFrame:
    profile = BROKER_PROFILES.get(broker_id)
    if profile is None:
        return df
    if profile.get("digit_convention", 5) == 4:
        point_cols = [c for c in df.columns if "Points" in c or "points" in c]
        for col in point_cols:
            if pd.api.types.is_numeric_dtype(df[col]):
                df[col] = df[col] * 10.0
                logging.info(f"[{broker_id}] Scaled {col} x10 (4-digit -> 5-digit).")
    return df

Для четырёхзначных брокеров значения во всех столбцах, содержащих строку «Points» (например, Avg_Slippage_Points), умножаются на 10 и тем самым приводятся к каноническому пятизначному представлению.

Нормализация комиссии (в базисных пунктах)

def normalize_commission(df: pd.DataFrame, broker_id: str) -> pd.DataFrame:
    profile            = BROKER_PROFILES.get(broker_id, {})
    commission_model   = profile.get("commission_model",   "spread_only")
    commission_per_lot = profile.get("commission_per_lot", 0.0)

    if "Total_Commission_Paid" not in df.columns:
        # Estimate from volume if missing
        vol_col = next((c for c in df.columns if "Volume" in c or "Lots" in c), None)
        if commission_model == "per_trade" and vol_col and pd.api.types.is_numeric_dtype(df[vol_col]):
            df["Total_Commission_Paid"] = df[vol_col].fillna(0) * commission_per_lot * 2.0
        else:
            df["Total_Commission_Paid"] = 0.0

    df["Total_Commission_Paid"] = pd.to_numeric(df["Total_Commission_Paid"], errors="coerce").fillna(0.0)

    vol_col = next((c for c in df.columns if "Volume" in c or "Lots" in c), None)
    if vol_col and pd.api.types.is_numeric_dtype(df[vol_col]):
        notional = df[vol_col].fillna(0.0) * 100_000.0
        df["Commission_BPS"] = np.where(
            notional > 0,
            (df["Total_Commission_Paid"] / notional) * 10_000.0,
            0.0
        )
    else:
        df["Commission_BPS"] = 0.0
    return df

Комиссия выражается в базисных пунктах от номинального объёма сделки, что позволяет напрямую сравнивать её независимо от размера сделки и валюты номинирования счёта.

Нормализация названий символов

def normalize_symbols(df: pd.DataFrame, broker_id: str) -> pd.DataFrame:
    profile = BROKER_PROFILES.get(broker_id, {})
    sym_map = profile.get("symbol_map", {})
    if "Symbol" not in df.columns:
        df["Symbol_Canonical"] = "UNKNOWN"
        return df
    df["Symbol_Canonical"] = df["Symbol"].map(sym_map)
    unmapped_mask = df["Symbol_Canonical"].isna()
    unmapped_symbols = df.loc[unmapped_mask, "Symbol"].unique()
    for sym in unmapped_symbols:
        logging.warning(f"[{broker_id}] Unmapped symbol: '{sym}'.")
    df.loc[unmapped_mask, "Symbol_Canonical"] = df.loc[unmapped_mask, "Symbol"]
    return df

Брокерские варианты названий символов (например, EURUSDm или Dow Jones) преобразуются в каноническую форму. Если соответствие не найдено, исходное название сохраняется, а предупреждение записывается в журнал.

Приведение временных меток к UTC

def normalize_timestamps(df: pd.DataFrame,
                          broker_id: str) -> pd.DataFrame:
    profile    = BROKER_PROFILES.get(broker_id, {})
    utc_offset = profile.get("server_utc_offset", 0)
    delta      = timedelta(hours=utc_offset)

    # Broader pattern: match any column containing "time" or "date"
    ts_patterns = ["time", "date", "timestamp"]
    ts_cols = [c for c in df.columns
               if any(p.lower() in c.lower() for p in ts_patterns)]

    for col in ts_cols:
        # Remove the deprecated infer_datetime_format parameter
        parsed = pd.to_datetime(df[col], errors="coerce")
        if parsed.notna().sum() > 0:
            df[col + "_UTC"] = parsed - delta
            logging.info(f"[{broker_id}] Normalized {col} "
                         f"(offset: -{utc_offset}h).")
    return df

Столбцы с временными метками определяются по шаблонам в их названиях, преобразуются в тип даты и времени, а затем переводятся в UTC с учётом смещения брокерского сервера.

Конвертация денежных значений в USD

def normalize_currency(df: pd.DataFrame, broker_id: str) -> pd.DataFrame:
    profile          = BROKER_PROFILES.get(broker_id, {})
    account_currency = profile.get("account_currency", "USD")
    monetary_cols    = ["Net_Profit", "Total_Commission_Paid", "Gross_Profit"]
    if account_currency == "USD":
        for col in monetary_cols:
            if col in df.columns:
                df[col + "_USD"] = df[col]
        return df
    fx_rate = STATIC_FX_RATES_TO_USD.get(account_currency)
    if fx_rate is None:
        logging.warning(f"[{broker_id}] No FX rate for '{account_currency}'.")
        return df
    for col in monetary_cols:
        if col in df.columns:
            df[col + "_USD"] = pd.to_numeric(df[col], errors="coerce").fillna(0.0) * fx_rate
            logging.info(f"[{broker_id}] {col} -> USD @ {fx_rate:.4f}.")
    return df

Статическая таблица обменных курсов переводит показатели прибыли и комиссии в USD. В рабочей системе её следует заменить актуальным или исторически точным источником валютных курсов.

Формирование единого набора данных

def build_unified_dataset(broker_file_map: dict) -> pd.DataFrame:
    frames = []
    for broker_id, file_path in broker_file_map.items():
        df = load_broker_export(file_path, broker_id)
        if df is None: continue
        df = normalize_pip_convention(df, broker_id)
        df = normalize_symbols(df, broker_id)
        df = normalize_timestamps(df, broker_id)
        df = normalize_commission(df, broker_id)
        df = normalize_currency(df, broker_id)
        profile = BROKER_PROFILES.get(broker_id, {})
        df["Broker_Display"] = profile.get("display_name", broker_id)
        frames.append(df)
    if not frames:
        raise ValueError("No broker exports loaded.")
    unified = pd.concat(frames, ignore_index=True, sort=False)
    # Column aliases and numeric type enforcement follow...
    return unified

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


Сравнительный анализ брокеров

Метрики качества исполнения

def summarize_execution_quality(unified: pd.DataFrame) -> pd.DataFrame:
    group_col = "Broker_Display" if "Broker_Display" in unified.columns else "Broker_ID"
    agg_dict = {}
    if "Commission_BPS"      in unified.columns: agg_dict["Commission_BPS"]      = "mean"
    if "Avg_Slippage_Points" in unified.columns: agg_dict["Avg_Slippage_Points"] = "mean"
    if "Total_Trades"        in unified.columns: agg_dict["Total_Trades"]        = "sum"
    summary = unified.groupby(group_col).agg(agg_dict).round(4)
    print("\n── Execution Quality Summary ──────────────────────────────")
    print(summary.to_string())
    return summary

Сравнение распределений проскальзывания

Функция plot_slippage_distributions() строит на общей оси KDE-кривые распределения Avg_Slippage_Points для каждого брокера и отмечает медианы вертикальными линиями. На график включаются только брокеры, для которых доступно не менее трёх наблюдений.

Slippage Distribution by Broker

Рис. 2. Распределение проскальзывания по брокерам. KDE-плотность нормализованного проскальзывания в пунктах для трёх брокерских профилей. Наложенные распределения показывают структурные различия в качестве исполнения.


Рейтинг результатов после учёта издержек

Функция plot_net_of_cost_ranking() строит сгруппированную столбчатую диаграмму, сравнивающую валовую и чистую прибыль после комиссии для каждого индикатора и брокера. При необходимости данные можно отфильтровать по каноническому символу.

Net-of-Cost Performance Ranking

Рис. 3. Рейтинг результатов после учёта издержек. Валовая и чистая прибыль в USD для индикатора EMA_Crossover на EURUSD у трёх брокеров. Разница между столбцами валовой и чистой прибыли соответствует комиссионным расходам.


Развёртывание и сопровождение 

Главный блок запускает весь конвейер, сохраняет канонический набор данных в normalized_outputs/unified_canonical.csv, выводит сводку по качеству исполнения и записывает обе визуализации в файлы PNG.

Брокерские профили необходимо периодически пересматривать по трём причинам:

  • Тарифы комиссий меняются; устаревшее значение commission_per_lot незаметно приведёт к неверному расчёту Commission_BPS.
  • Брокеры иногда изменяют правила именования символов, из-за чего появляются несопоставленные значения, отмеченные предупреждениями в журнале аудита.
  • Брокеры из регионов с переходом на летнее время сезонно переключаются между двумя смещениями UTC.

Файл normalization_audit.log регистрирует каждый несопоставленный символ, каждую ошибку преобразования временной метки и каждую статическую валютную конвертацию. Канонический CSV напрямую совместим со всеми функциями из Частей 3 и 4; столбец Broker_Display добавляет новое измерение группировки для каждого графика.


Проверка и тестирование

Перед публикацией каждый компонент был проверен по систематическому протоколу тестирования.

Синтетические тестовые данные

Отдельный скрипт generate_test_exports.py создаёт три CSV-файла в каталоге exports/. Каждый файл содержит 10 строк с реалистичным разбросом Avg_Slippage_Points, полученным из нормальных распределений, благодаря чему KDE-графики позволяют построить информативные кривые плотности.

  • Брокер A — пятизначное котирование, комиссия за сделку, счёт в USD, UTC+2, символ EURUSD
  • Брокер B — пятизначное котирование, комиссия включена в спред, счёт в EUR, UTC+3, символ EURUSDm
  • Брокер C — четырёхзначное котирование, комиссия за сделку, счёт в GBP, UTC+0, символ Dow Jones

Проверка с помощью инструкций assert

После запуска нормализатора корректность каждого преобразования проверяется следующими инструкциями assert:

import pandas as pd
df = pd.read_csv("normalized_outputs/unified_canonical.csv")

# Currency conversion: BrokerB EUR -> USD
assert round(df[df["Broker_ID"]=="BrokerB"]["Net_Profit_USD"].iloc[0], 1) == 432.8
# Currency conversion: BrokerC GBP -> USD
assert round(df[df["Broker_ID"]=="BrokerC"]["Net_Profit_USD"].iloc[0], 1) == 379.2
# Pip scaling: BrokerC Avg_Slippage_Points multiplied by 10
assert df[df["Broker_ID"]=="BrokerC"]["Avg_Slippage_Points"].iloc[0] > 10
# Symbol canonicalization: EURUSDm -> EURUSD, Dow Jones -> US30
assert set(df["Symbol_Canonical"]) == {"EURUSD", "US30"}
# Timestamps: BrokerA UTC+2 08:00 -> 06:00 UTC
assert pd.to_datetime(df[df["Broker_ID"]=="BrokerA"]["Open_Time_UTC"].iloc[0]).hour == 6
print("All assertions passed.")

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


Заключение

Конвейер анализа одного брокера предполагает степень единообразия данных, которой в реальных брокерских средах просто не существует. Если без предварительной обработки объединить CSV-экспорты нескольких брокеров, различия в разрядности котирования и соглашениях о пунктах, именовании символов, смещениях часовых поясов, учёте комиссии и валютах счёта и валютах, в которых выражена прибыль незаметно исказят все сравнительные расчёты. В статье эта проблема решена с помощью расширения экспорта, учитывающего брокера, и слоя нормализации Python, которые совместно преобразуют разнородные исходные данные в единый канонический набор, пригодный для анализа.

Решение состоит из двух взаимодополняющих компонентов. На стороне MetaTrader 5 файл BrokerAwareExporter.mqh дополняет каждую экспортируемую строку брокерскими метаданными — именем сервера, валютой номинирования счёта, количеством знаков символа, измеренной комиссией и проскальзыванием, — поэтому на последующих этапах ручная маркировка не требуется. На стороне Python модуль broker_normalizer.py использует декларативный реестр брокерских профилей для устранения всех источников расхождений схем: приводит значения пунктов четырёхзначного котирования к пятизначному формату, сопоставляет брокерские псевдонимы символов с каноническими именами, переводит временные метки в UTC, выражает комиссию в базисных пунктах от номинального объёма и конвертирует все денежные поля в USD. Результирующий файл unified_canonical.csv соответствует фиксированной схеме и напрямую совместим с функциями визуализации и анализа, разработанным в Частях 3 и 4.

После формирования единого набора данных сравнение брокеров становится стандартной процедурой. У какого брокера проскальзывание ниже? Насколько комиссия сокращает преимущество стратегии? Какую долю теоретического преимущества поглощают издержки на каждой площадке? Сохраняется ли рейтинг результатов у разных брокеров или меняется после учёта качества исполнения? Сравнительные графики распределения проскальзывания и результатов после учёта издержек позволяют быстро увидеть эти различия, а набор проверок assert обеспечивает уверенность в корректности нормализации.

Реестр брокерских профилей рассчитан на сопровождение, а не на переписывание. Чтобы добавить нового брокера в конвейер, достаточно создать новую запись в словаре; изменять логику нормализации не требуется. Периодическая проверка профилей с опорой на normalization_audit.log позволяет учитывать меняющиеся условия брокеров. Вся система интегрируется без дополнительных доработок с потоковой обработкой в реальном времени из Части 5 и аналитикой оптимизации из предыдущих частей, формируя полноценный фреймворк оценки стратегий, не зависящий от брокера.

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


Программы, использованные в статье:

# Название Тип Описание
1 BrokerAwareExporter.mqh Подключаемый файл Расширяет базовую схему CSV-экспорта брокерскими метаданными; автоматически заполняет имя сервера, валюту счёта, количество знаков символа, комиссию и проскальзывание.
2 Test_BrokerAware_EA.mq5 Демонстрационный советник Минимальный советник, показывающий подключение и вызов экспортёра с учётом брокера; на первом тике записывает одну тестовую строку.
3 generate_test_exports.py Скрипт Python Создаёт многострочные синтетические CSV-файлы для трёх вымышленных брокеров с разными значениями проскальзывания для тестирования нормализатора.
4 broker_normalizer.py Скрипт Python Полный конвейер нормализации: загружает брокерские экспорты, нормализует разрядность пунктов, названия символов, временные метки, комиссию и валюту; строит единый канонический набор данных и формирует сравнительные графики.
5 assertion_based_validation.py Скрипт Python Проверяет корректность результата нормализатора с помощью инструкций assert для валютной конвертации, масштабирования пунктов, канонизации символов и смещений временных меток UTC.
6 CSV_Data_Analysis_Part_6 ZIP-архив ZIP-архив, содержащий все приложенные файлы и пути к ним относительно корневого каталога терминала.

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

Автоматизация торговых стратегий на MQL5 (Часть 36): Торговля по зонам спроса и предложения на основе модели ретеста и импульса Автоматизация торговых стратегий на MQL5 (Часть 36): Торговля по зонам спроса и предложения на основе модели ретеста и импульса
В этой статье мы создадим на MQL5 торговую систему на основе зон спроса и предложения. Она определяет такие зоны по диапазонам консолидации, подтверждает их импульсными движениями и открывает позиции на ретестах с подтверждением тренда и настраиваемыми параметрами риска. Система отображает зоны с помощью динамических подписей и цветов, а также поддерживает трейлинг-стопы для управления рисками.
Возможности Мастера MQL5, которые вам нужно знать (Часть 82): Использование паттернов TRIX и WPR в обучении с подкреплением DQN Возможности Мастера MQL5, которые вам нужно знать (Часть 82): Использование паттернов TRIX и WPR в обучении с подкреплением DQN
В предыдущей статье мы рассмотрели сочетание Ишимоку и ADX в рамках обучения на основе инференса. В этой статье мы вновь обращаемся к обучению с подкреплением применительно к паре индикаторов, которую уже рассматривали в части 68: TRIX и Williams Percent Range. В этой статье используем алгоритм алгоритм DQN с квантильной регрессией. Как обычно, представляем его в виде пользовательского класса сигнала, предназначенного для использования с Мастером MQL5.
Моделирование рынка: Position View (X) Моделирование рынка: Position View (X)
Нам необходим способ обработки создаваемых графических объектов. Предложение, представленное в предыдущей статье, отлично подходит для некоторых сценариев. В данном случае нам потребуется нечто более сложное, учитывая специфику рассматриваемой проблемы. Поэтому мы не будем пытаться заменить существующие в MetaTrader 5 механизмы управления ZOrder и, конечно же, проверять, какой объект находится на переднем плане или скрыт другим объектом. Мы сделаем нечто совершенно другое. Здесь я покажу, какие изменения необходимо внести в код, чтобы задействовать часть того, что MetaTrader 5 уже делает за нас.
Автоматизация торговых стратегий на MQL5 (Часть 35): Разработка торговой системы на основе брейкер-блоков Автоматизация торговых стратегий на MQL5 (Часть 35): Разработка торговой системы на основе брейкер-блоков
В этой статье мы создадим на MQL5 торговую систему на основе брейкер-блоков. Она определяет диапазоны консолидации, обнаруживает пробои и подтверждает брейкер-блоки по свинговым точкам, чтобы торговать их ретесты с заданными параметрами риска. Система динамически отображает ордер-блоки и брейкер-блоки, а также связанные с ними подписи и стрелки; кроме того, система поддерживает автоматическую торговлю и использование трейлинг-стопа.