Анализ CSV-данных (Часть 6): Нормализация результатов по нескольким брокерам и сверка CSV между платформами
Введение
В предыдущих статьях этой серии мы построили конвейер экспорта и анализа на основе единого единого соглашения о структуре данных: один советник, один брокер, одна схема. На практике профессиональная разработка стратегий редко ограничивается этими рамками. Оцениваемая стратегия может одновременно работать на реальном счёте у одного брокера и на демосчёте у другого для сравнения качества исполнения сделок. В ходе оптимизации одни и те же параметры могут перебираться в средах двух брокеров, чтобы выявить зависимость результатов от брокера. Портфель стратегий может быть распределён между несколькими брокерами для диверсификации контрагентского риска, причём каждый счёт формирует отдельный 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 столбцов — стандартные показатели эффективности и брокерские метаданные — и позволяет проверить структуру схемы. Ниже приведён пример результата.

Рис. 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 для каждого брокера и отмечает медианы вертикальными линиями. На график включаются только брокеры, для которых доступно не менее трёх наблюдений.

Рис. 2. Распределение проскальзывания по брокерам. KDE-плотность нормализованного проскальзывания в пунктах для трёх брокерских профилей. Наложенные распределения показывают структурные различия в качестве исполнения.
Рейтинг результатов после учёта издержек
Функция plot_net_of_cost_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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Автоматизация торговых стратегий на MQL5 (Часть 36): Торговля по зонам спроса и предложения на основе модели ретеста и импульса
Возможности Мастера MQL5, которые вам нужно знать (Часть 82): Использование паттернов TRIX и WPR в обучении с подкреплением DQN
Моделирование рынка: Position View (X)
Автоматизация торговых стратегий на MQL5 (Часть 35): Разработка торговой системы на основе брейкер-блоков
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования