Анализ CSV-данных (Часть 4): Разработка автоматизированного Python-решения для сравнительного анализа и валидации стратегий MQL5
Введение
MetaTrader 5 располагает мощным встроенным механизмом оптимизации. Тестер стратегий формирует сводную статистику, кривые капитала и табличные результаты для различных наборов параметров. Однако при исследовании нескольких активов на нескольких таймфреймах с пользовательскими счётчиками встроенный слой вывода результатов становится узким местом. Исследователь, сравнивающий четыре варианта индикатора на шести валютных парах и трёх таймфреймах, получает около 72 самостоятельных наборов результатов. Ручная проверка ранжирования с учётом риска, сопоставление параметров при walk-forward-валидации и подготовка текста аналитических выводов требуют много времени. Кроме того, такой подход усиливает селективное внимание и повышает риск переобучения (curve-fitting) под исторические данные.
Решение состоит в том, чтобы использовать MetaTrader 5 исключительно как механизм генерации данных. Тестер выполняет расчёты, структурированный файл экспорта сохраняет результаты, после чего Python берёт на себя парсинг, сортировку, сопоставление, визуализацию и автоматическое формирование текста аналитических выводов. Весь аналитический пайплайн становится воспроизводимым, аудируемым и менее подверженным когнитивным упрощениям, к которым аналитик неизбежно прибегает при ручном просмотре сотен строк оптимизации.
В этой статье пайплайн создаётся с нуля. Определяется необходимая схема экспорта MQL5, описываются два аналитических модуля Python и приводится процесс развёртывания и поддержки обоих компонентов.
Примечание: Этот пайплайн расширяет набор средств Python для обработки данных и визуализации, представленный в Части 3.
Раздел 1. Проектирование схемы экспорта результатов оптимизации
Качество пайплайна определяется строгостью соглашения о формате данных. До написания первой строки кода Python стратегия MQL5 должна экспортировать данные по строгой и предсказуемой схеме. Каждый столбец должен присутствовать, каждая строка должна содержать собственный контекст, а правила именования должны быть достаточно стабильными, чтобы парсеру Python не приходилось делать предположения.
Обязательная структура столбцов
Основной CSV-файл экспорта должен содержать как минимум следующие столбцы:
| Столбец | Тип | Описание |
|---|---|---|
| Test_Phase | Строка | Определяет строку как Baseline, InSample или OutSample |
| Symbol | Строка | Торгуемый инструмент (например, EURUSD или XAUUSD) |
| Timeframe | Строка | Период графика (например, M15, H1 или H4) |
| Indicator_Name | Строка | Тестируемый фильтр (например, SMA, EMA, KAMA или SAMA) |
| Filter_Period | Целое число | Период расчёта индикатора, использованный в данном тесте |
| Param_SAMA_LR | Число с плавающей точкой | Адаптивный параметр конкретной стратегии (пустое значение для вариантов, отличных от SAMA) |
| Net_Profit_$ | Число с плавающей точкой | Абсолютная чистая прибыль за период тестирования |
| Sortino_Ratio | Число с плавающей точкой | Показатель доходности, скорректированный на отрицательное отклонение |
| False_Flips_Whipsaws | Целое число | Количество ложных сигналов пересечения за время теста |
| Avg_Lag_On_Turn_Bars | Число с плавающей точкой | Средняя задержка реакции индикатора на разворот цены, выраженная в барах |
Поле Test_Phase является наиболее важным структурным столбцом схемы. Оно служит основным полем разбиения и маркировки данных. С его помощью базовые записи отделяются от проходов оптимизации, а внутри оптимизационных проходов обучающие окна InSample — от валидационных окон OutSample. Без этого поля модуль Python не сможет автоматически разделять данные.
Столбцы False_Flips_Whipsaws и Avg_Lag_On_Turn_Bars представляют собой пользовательские математические счётчики. В стандартном выводе MT5 их нет: они рассчитываются непосредственно внутри стратегии MQL5, а затем записываются в файл экспорта вместе со стандартными показателями эффективности. Эти счётчики количественно характеризуют компромисс между задержкой и шумом, который невозможно описать только показателями прибыли или просадки.
Запись экспортных данных в MQL5
Следующий структурный каркас показывает, как стратегия заполняет и экспортирует эту схему в рамках конкретной фазы тестирования. По завершении каждого теста в указанный CSV-файл записывается одна строка. Параметр Test_Phase передаётся извне либо переключается через входную переменную. Благодаря этому один и тот же скомпилированный файл .ex5 может использоваться на всех трёх этапах без повторной компиляции.
//--- Export configuration inputs input string InpTestPhase = "Baseline"; // Test_Phase tag for this run input string InpSymbol = "EURUSD"; // Traded symbol identifier input string InpTimeframe = "H1"; // Chart period label input string InpIndicatorName = "SAMA"; // Filter under test input int InpFilterPeriod = 14; // Lookback period input double InpSAMA_LR = 0.0; // Adaptive param (0 if N/A) input string InpExportPath = "results.csv"; // Output file name //--- Persistent counters accumulated during the test int g_FalseFlips = 0; double g_TotalLagBars = 0.0; int g_TurnCount = 0; //+------------------------------------------------------------------+ //| Write a single result row to the master CSV | //+------------------------------------------------------------------+ void ExportResultRow(double netProfit,double sortinoRatio) { //--- Build the lag average from accumulated counters double avgLag = (g_TurnCount > 0) ? (g_TotalLagBars / g_TurnCount) : 0.0; //--- Open the file in append mode, creating it if absent int handle = FileOpen(InpExportPath,FILE_WRITE|FILE_READ|FILE_CSV|FILE_ANSI|FILE_SHARE_READ,','); if(handle == INVALID_HANDLE) { Print("ExportResultRow: failed to open file ",InpExportPath); return; } //--- Move to end of file for append behavior FileSeek(handle,0,SEEK_END); //--- Write the structured result row FileWrite(handle, InpTestPhase, InpSymbol, InpTimeframe, InpIndicatorName, IntegerToString(InpFilterPeriod), DoubleToString(InpSAMA_LR,4), DoubleToString(netProfit,2), DoubleToString(sortinoRatio,4), IntegerToString(g_FalseFlips), DoubleToString(avgLag,2)); FileClose(handle); } //+------------------------------------------------------------------+ //| Accumulate a whipsaw event during signal processing | //+------------------------------------------------------------------+ void RecordFalseFlip() { g_FalseFlips++; } //+------------------------------------------------------------------+ //| Accumulate a directional turn lag measurement | //+------------------------------------------------------------------+ void RecordTurnLag(int lagBars) { g_TotalLagBars += lagBars; g_TurnCount++; }
Схема открытия файла использует сочетание FILE_WRITE | FILE_READ и добавляет данные в конец файла с помощью FileSeek; при этом безопасность одновременной записи не гарантируется. Каждый проход записывает ровно одну строку. После полного цикла оптимизации CSV-файл содержит по одной строке для каждой комбинации параметров и каждой метки этапа.
Инициализация строки заголовков CSV
Заголовок следует записывать ровно один раз — как правило, в OnInit(), если файл ещё не существует. Следующая функция реализует эту проверку:
//+------------------------------------------------------------------+ //| Write CSV header if file does not already exist | //+------------------------------------------------------------------+ void InitExportHeader() { //--- Check for an existing file before writing the header if(FileIsExist(InpExportPath,FILE_COMMON)) { return; } int handle = FileOpen(InpExportPath,FILE_WRITE|FILE_CSV|FILE_ANSI,','); if(handle == INVALID_HANDLE) { Print("InitExportHeader: cannot create file ",InpExportPath); return; } //--- Write column header row FileWrite(handle, "Test_Phase", "Symbol", "Timeframe", "Indicator_Name", "Filter_Period", "Param_SAMA_LR", "Net_Profit_$", "Sortino_Ratio", "False_Flips_Whipsaws", "Avg_Lag_On_Turn_Bars"); FileClose(handle); }
После добавления этой проверки заголовок записывается один раз независимо от количества оптимизационных проходов Тестера. Все последующие вызовы ExportResultRow аккуратно добавляют данные под ним.
Раздел 2. Базовый экспорт по нескольким активам и таймфреймам
Базовый этап отвечает на один конкретный вопрос: чем структурно отличается поведение вариантов при тестировании в одинаковых фиксированных условиях? На этом этапе оптимизация не выполняется. Параметр фиксируется на одном типовом значении — обычно на стандартном периоде 14. Затем каждый вариант индикатора тестируется на одном и том же диапазоне дат с одинаковыми инструментами и таймфреймами.
Схема экспорта поддерживает этот процесс автоматически. Каждая строка, записанная во время базового прогона, получает метку Test_Phase = "Baseline". При чтении файла Python фильтрует данные именно по этой метке и извлекает записи, необходимые для сравнения при одинаковых параметрах. Поэтому риск случайно примешать данные оптимизации отсутствует.
Базовое тестирование на M15, H1 и H4 создаёт по одной строке на каждый таймфрейм и инструмент для каждого индикатора. Общее количество строк зависит от числа инструментов.
Раздел 3. Обработка данных при walk-forward-валидации без смещения из-за подгонки под историю
Представление оптимального результата InSample как доказательства состоятельности стратегии может стать серьёзной методологической ошибкой. По своей природе цикл оптимизации лишь находит параметры, лучше всего соответствующие историческим данным. Такое соответствие редко отражает будущие результаты и зачастую вообще не обладает прогностической ценностью. Для подтверждения реальной устойчивости необходимо применить те же самые параметры к полностью новым данным.
Модуль Python выполняет это сопоставление без ручного вмешательства. Логика проста, но должна быть реализована точно.
Проблема смещения на практике
Предположим, что оптимизационный проход для SAMA на EURUSD создаёт двадцать строк InSample. Каждая строка соответствует отдельной комбинации Filter_Period и Param_SAMA_LR. Та же оптимизация формирует двадцать соответствующих строк OutSample. Возникает соблазн выбрать лучшую строку OutSample и представить её в отчёте. Однако это фактически означает проведение второй оптимизации на новых, ранее не виденных данных, то есть остаётся формой подгонки к истории.
Правильный подход — кросс-валидация с зафиксированными параметрами:
- Найдите строку с максимальным коэффициентом Сортино в подмножестве InSample.
- Зафиксируйте точные значения Filter_Period и Param_SAMA_LR.
- Найдите строку OutSample с теми же точными значениями параметров.
- Представьте показатели этой строки OutSample как результат на слепой выборке.
Шаг 3 является принципиальным отличием. Любое отклонение от точного совпадения параметров вновь вносит смещение из-за подгонки под историю. Например, нельзя усреднять соседние периоды или выбирать «ближайшую» строку OutSample по другому показателю.
Логика сопоставления в Python
Модуль walk-forward реализует эту процедуру следующим образом:
best_is_idx = is_data['Sortino_Ratio'].idxmax() best_is_row = is_data.loc[best_is_idx] best_period = best_is_row['Filter_Period'] best_lr = best_is_row.get('Param_SAMA_LR', 0) matching_oos = oos_data[ (oos_data['Filter_Period'] == best_period) & ( (oos_data['Param_SAMA_LR'] == best_lr) | (oos_data['Param_SAMA_LR'].isna()) ) ]
Вызов idxmax() для столбца Sortino в наборе InSample возвращает целочисленный индекс глобально лучшей строки, а не условно лучшей или лучшей по рангу строки. Значения параметров, извлечённые именно из неё, затем используются как фильтры точного равенства для подмножества OutSample. Условие isna() во второй части обрабатывает варианты, отличные от SAMA, для которых Param_SAMA_LR закономерно не задан. Благодаря этому механизм сопоставления не отбрасывает незаметно строки OutSample для SMA, EMA и KAMA.
В результате для каждого индикатора и инструмента выбираются ровно одна строка InSample и одна строка OutSample. Такое сравнение однозначно и свободно от систематической ошибки отбора.
Раздел 4. Аналитический модуль Python
Аналитическая часть разделена на два файла, соответствующих двум различным этапам тестирования. Каждый файл автономен: он читает собственный входной CSV-файл, создаёт отдельный текстовый отчёт и сохраняет собственные изображения. Скрипты можно запускать независимо либо последовательно из сценария командной оболочки.
Скрипт A. Базовый механизм сравнительного анализа при одинаковых параметрах
Сохраните как baseline_analysis.py
import pandas as pd import matplotlib.pyplot as plt import dataframe_image as dfi import os # --- Configuration CSV_FILE = 'baseline_results.csv' OUTPUT_IMAGE_TABLE = 'Baseline_Table.png' OUTPUT_TEXT = 'Baseline_Analysis_Report.txt' def get_metric(df_summary, indicator, column): vals = df_summary.loc[df_summary['Indicator_Name'] == indicator, column].values return float(vals[0]) if len(vals) > 0 else 0.0 def run_baseline_analysis(): if not os.path.exists(CSV_FILE): print(f"Error: {CSV_FILE} not found.") return df = pd.read_csv(CSV_FILE) df = df[df['Test_Phase'].str.contains('Baseline', case=False, na=False)] report_text = "# Parameter-Matched Comparative Analysis (Baseline)\n\n" report_text += ( "*Note: The following data represents the average performance of a fixed " "14-period matched test across M15, H1, and H4 timeframes.*\n\n" ) symbols = df['Symbol'].unique() for symbol in symbols: report_text += f"### {symbol} Parameter-Matched Analysis\n\n" symbol_data = df[df['Symbol'] == symbol] # --- Cross-timeframe aggregation: mean per indicator across M15, H1, H4 summary = symbol_data.groupby('Indicator_Name').agg({ 'Net_Profit_$' : 'mean', 'Sortino_Ratio' : 'mean', 'False_Flips_Whipsaws' : 'mean', 'Avg_Lag_On_Turn_Bars' : 'mean' }).reset_index() sama_profit = get_metric(summary, 'SAMA', 'Net_Profit_$') ema_profit = get_metric(summary, 'EMA', 'Net_Profit_$') sama_sortino = get_metric(summary, 'SAMA', 'Sortino_Ratio') kama_sortino = get_metric(summary, 'KAMA', 'Sortino_Ratio') sma_sortino = get_metric(summary, 'SMA', 'Sortino_Ratio') sama_fc = get_metric(summary, 'SAMA', 'False_Flips_Whipsaws') sma_fc = get_metric(summary, 'SMA', 'False_Flips_Whipsaws') ema_fc = get_metric(summary, 'EMA', 'False_Flips_Whipsaws') sama_lag = get_metric(summary, 'SAMA', 'Avg_Lag_On_Turn_Bars') sma_lag = get_metric(summary, 'SMA', 'Avg_Lag_On_Turn_Bars') ema_lag = get_metric(summary, 'EMA', 'Avg_Lag_On_Turn_Bars') profit_comparison = "higher than" if sama_profit > ema_profit else "lower than" report_text += "**1. Comparative Backtest (SMA vs. EMA vs. KAMA vs. SAMA)**\n" report_text += ( f"To determine the practical edge of the SAMA filter, a comparative analysis was " f"conducted against standard SMA, EMA, and the adaptive KAMA. " f"Looking at the baseline performance on {symbol}, SAMA demonstrated a distinct profile. " f"While the Net Profit of SAMA (${sama_profit:.2f}) was {profit_comparison} the EMA " f"(${ema_profit:.2f}), the true advantage emerged in the risk metrics. " f"SAMA achieved a Sortino Ratio of {sama_sortino:.2f}, compared to KAMA's " f"{kama_sortino:.2f} and SMA's {sma_sortino:.2f}, indicating a specific equity curve " f"risk profile. " f"Furthermore, SAMA recorded an average of {sama_fc:.1f} false flips during the test " f"period, contrasting with the SMA which produced {sma_fc:.1f} false flips.\n\n" ) report_text += "**2. The Lag vs. Noise Trade-off**\n" report_text += ( f"Every moving average faces the fundamental trade-off between lag (responsiveness) " f"and noise (false signals). " f"In the exported data, we analyzed the average lag on turn against the frequency of " f"false crossovers. " f"Traditional indicators force a compromise: reducing lag exponentially increases false flips. " f"Because of its adaptive FIR nature, SAMA maintained an average lag of {sama_lag:.1f} bars " f"while suppressing false flips to {sama_fc:.1f}. " f"For context on this {symbol} curve, the EMA had a lag of {ema_lag:.1f} bars with " f"{ema_fc:.1f} false flips, " f"while the SMA presented a lag of {sma_lag:.1f} bars yielding {sma_fc:.1f} false flips.\n\n" ) report_text += "---\n\n" # --- Per-symbol dual-panel histogram (980px width constraint) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9.8, 5), dpi=100) plot_data = summary.set_index('Indicator_Name') plot_data['Sortino_Ratio'].plot( kind='bar', color='#1f77b4', ax=ax1, edgecolor='black' ) ax1.set_title(f'Risk: Average Sortino Ratio ({symbol})') ax1.set_ylabel('Sortino Ratio') ax1.set_xlabel('Indicator') ax1.invert_yaxis() # Inverted so least-negative bar reads as visually tallest ax1.tick_params(axis='x', rotation=0) plot_data['False_Flips_Whipsaws'].plot( kind='bar', color='#d62728', ax=ax2, edgecolor='black' ) ax2.set_title(f'Noise: Average False Flips ({symbol})') ax2.set_ylabel('False Flips (Whipsaws)') ax2.set_xlabel('Indicator') ax2.tick_params(axis='x', rotation=0) fig.suptitle( f'Baseline Comparison: Sortino Ratio vs Noise ({symbol})', fontsize=14, fontweight='bold' ) fig.tight_layout() plt.savefig(f'Baseline_Histogram_{symbol}.png') plt.close() # --- Aggregate summary table across all symbols overall_summary = df.groupby('Indicator_Name')[ ['Sortino_Ratio', 'False_Flips_Whipsaws'] ].mean() styled_table = ( overall_summary.style .background_gradient(cmap='Blues') .format("{:.2f}") ) dfi.export(styled_table, OUTPUT_IMAGE_TABLE, max_cols=-1, max_rows=-1) with open(OUTPUT_TEXT, 'w') as f: f.write(report_text) print(f"Baseline analysis complete. Report: {OUTPUT_TEXT}") if __name__ == "__main__": run_baseline_analysis()
Пошаговое описание работы скрипта:
После проверки наличия CSV-файла модуль сразу фильтрует столбец Test_Phase, чтобы оставить только базовые записи. Затем он перебирает все уникальные инструменты в наборе данных. Для каждого инструмента данные группируются по Indicator_Name, после чего рассчитываются средние значения по трём таймфреймам: M15, H1 и H4 агрегируются в одну строку для каждого индикатора. Именно такое межтаймфреймовое усреднение позволяет называть результат сравнением при одинаковых параметрах: выводы не определяются одним отдельным таймфреймом.
Текстовые блоки отчёта формируются программно с помощью форматированных строк. Переменная profit_comparison автоматически выбирает корректную формулировку направления сравнения, поэтому текст остаётся правильным независимо от того, какой индикатор показал лучший результат. Для каждого инструмента строится гистограмма шириной ровно 980 пикселей (9,8 дюйма при 100 DPI), на которой рядом отображаются коэффициент Сортино и количество ложных срабатываний.
Инверсия оси Y на панели Сортино (ax1.invert_yaxis()) выполнена намеренно. Если коэффициенты Сортино отрицательны — что бывает у убыточных стратегий или в периоды высоковолатильных просадок, — после инверсии наименее отрицательный столбец визуально становится самым «высоким». Это сохраняет интуитивное направление чтения и не искажает данные.
Последний вызов dfi.export преобразует оформленную сводную таблицу в PNG с помощью библиотеки dataframe_image. Изображение отражает агрегированные результаты по всем инструментам и служит справочной таблицей статьи.

Рис. 1. Базовая таблица, созданная скриптом baseline_analysis.py
Скрипт B. Модуль walk-forward-кросс-валидации
Сохраните как optimized_analysis.py
import pandas as pd import matplotlib.pyplot as plt import dataframe_image as dfi import os # --- Configuration CSV_FILE = 'optimized_results.csv' OUTPUT_IMAGE_HIST = 'WalkForward_Histogram.png' OUTPUT_IMAGE_TABLE = 'WalkForward_Table.png' OUTPUT_TEXT = 'WalkForward_Analysis_Report.txt' def run_optimized_analysis(): if not os.path.exists(CSV_FILE): print(f"Error: {CSV_FILE} not found.") return df = pd.read_csv(CSV_FILE) report_text = "# Walk-Forward Optimized Analysis\n\n" symbols = df['Symbol'].unique() final_comparison_rows = [] for symbol in symbols: report_text += f"## Walk-Forward Analysis for {symbol}\n\n" symbol_data = df[df['Symbol'] == symbol] for indicator in symbol_data['Indicator_Name'].unique(): ind_data = symbol_data[symbol_data['Indicator_Name'] == indicator] is_data = ind_data[ind_data['Test_Phase'].str.contains( 'InSample', case=False, na=False)] oos_data = ind_data[ind_data['Test_Phase'].str.contains( 'OutSample', case=False, na=False)] if is_data.empty or oos_data.empty: continue # --- Locate the highest Sortino row in the InSample set best_is_idx = is_data['Sortino_Ratio'].idxmax() best_is_row = is_data.loc[best_is_idx] best_period = best_is_row['Filter_Period'] best_lr = best_is_row.get('Param_SAMA_LR', 0) # --- Match OutSample row using exact parameter values (no substitution) matching_oos = oos_data[ (oos_data['Filter_Period'] == best_period) & ( (oos_data['Param_SAMA_LR'] == best_lr) | (oos_data['Param_SAMA_LR'].isna()) ) ] if not matching_oos.empty: best_oos_row = matching_oos.iloc[0] final_comparison_rows.append({ 'Symbol' : symbol, 'Indicator' : indicator, 'IS_Sortino' : best_is_row['Sortino_Ratio'], 'OOS_Sortino': best_oos_row['Sortino_Ratio'], 'IS_Profit' : best_is_row['Net_Profit_$'], 'OOS_Profit' : best_oos_row['Net_Profit_$'] }) profit_drop = ( (best_is_row['Net_Profit_$'] - best_oos_row['Net_Profit_$']) / best_is_row['Net_Profit_$'] ) * 100 report_text += ( f"**{indicator}**: The optimal parameters from the InSample phase " f"(Period: {best_period}) " f"yielded a Sortino Ratio of {best_is_row['Sortino_Ratio']:.2f}. " f"When these exact parameters were applied to the blind OutSample data, " f"the Sortino Ratio shifted to {best_oos_row['Sortino_Ratio']:.2f}, " f"with a profit variance of {profit_drop:.1f}%.\n" ) report_text += "\n" if not final_comparison_rows: print("No matched IS/OOS pairs found. Check Test_Phase labels in the CSV.") return results_df = pd.DataFrame(final_comparison_rows) # --- Walk-forward degradation histogram (980px layout constraint) fig, ax = plt.subplots(figsize=(9.8, 6), dpi=100) plot_data = results_df.groupby('Indicator')[['IS_Sortino', 'OOS_Sortino']].mean() plot_data.plot(kind='bar', ax=ax) plt.title('Walk-Forward Validation: InSample vs OutSample Sortino Ratio Degradation (IS vs OOS)') plt.ylabel('Sortino Ratio') plt.xticks(rotation=0) fig.tight_layout() plt.savefig(OUTPUT_IMAGE_HIST) plt.close() # --- Styled summary table image styled_table = ( plot_data.style .background_gradient(cmap='Purples') .format("{:.2f}") ) dfi.export(styled_table, OUTPUT_IMAGE_TABLE, max_cols=-1, max_rows=-1) with open(OUTPUT_TEXT, 'w') as f: f.write(report_text) print(f"Walk-forward analysis complete. Report: {OUTPUT_TEXT}") if __name__ == "__main__": run_optimized_analysis()
Об измерении деградации:
Расчёт profit_drop измеряет пропорциональное ухудшение результатов между максимумом InSample и результатом OutSample при использовании тех же параметров. Значение profit_drop, близкое к нулю, указывает на хорошую способность оптимизированных параметров к обобщению. Большое положительное значение profit_drop — 60% и более — служит количественным признаком подгонки к истории (curve-fitting): параметры были настроены на исторический шум, а не на реальное торговое преимущество.
Итоговая гистограмма группирует результаты по индикаторам и усредняет коэффициенты Сортино IS и OOS по всем протестированным инструментам. Такое агрегированное представление показывает, является ли профиль оптимизации индикатора устойчивым в целом или сохраняется только при отдельных рыночных условиях.
Раздел 5. Настройка среды и развёртывание
Структура каталогов
Перед запуском любого из скриптов организуйте рабочий каталог следующим образом:
/research_workspace/ ├── baseline_results.csv # Baseline phase export from MT5 ├── optimized_results.csv # Optimization phase export from MT5 ├── baseline_analysis.py # Script A ├── optimized_analysis.py # Script B └── /outputs/ # Auto-generated; scripts write here ├── Baseline_Analysis_Report.txt ├── Baseline_Table.png ├── Baseline_Histogram_EURUSD.png ├── Baseline_Histogram_XAUUSD.png ├── WalkForward_Analysis_Report.txt ├── WalkForward_Histogram.png └── WalkForward_Table.png
Оба скрипта записывают выходные файлы в текущий рабочий каталог, если константы OUTPUT_* в начале файлов не изменены так, чтобы включать путь к подкаталогу. Сохранение результатов в отдельную папку /outputs/ позволяет не загромождать корневой каталог и упрощает поиск материалов для публикации.
Имена CSV-файлов в константах CSV_FILE должны совпадать с фактическими именами файлов, экспортированных из MT5. Несовпадение имён — самая распространённая причина скрытых сбоев в этом пайплайне.
Установка зависимостей
Все необходимые библиотеки доступны через pip. Выполните следующую команду в терминале с активированной целевой средой Python:
pip install pandas matplotlib dataframe_image
Библиотека dataframe_image использует экземпляр Chromium без графического интерфейса либо совместимый системный браузер для преобразования оформленных DataFrame в PNG. На Linux-серверах без графической оболочки зависимости Chromium необходимо установить отдельно:
pip install dataframe_image[all] Если механизм визуализации выдаёт ошибку инициализации браузера, dataframe_image также поддерживает резервный вариант на основе matplotlib через аргумент backend='matplotlib' функции dfi.export(). Он создаёт приемлемый результат с незначительными отличиями в оформлении.
Требуется Python 3.6 или новее. После установки проверьте версии библиотек:
python -c "import pandas; import matplotlib; import dataframe_image; print('All dependencies loaded.')" Запуск пайплайна
Сначала запустите базовый анализ:
python baseline_analysis.py
Затем запустите модуль walk-forward:
python optimized_analysis.py
При корректном завершении оба скрипта выводят сообщение в stdout. Отсутствующий файл или неожиданное имя столбца приводят к явному сообщению об ошибке, а не к тихому сбою.
Ограничение ширины 980 пикселей
Оба скрипта используют figsize=(9.8, X) при dpi=100, что даёт изображение шириной 980 пикселей. Это ограничение предотвращает горизонтальный выход за границы на стандартных торговых порталах и страницах статей MQL5, где ширина области содержимого составляет от 900 до 1000 пикселей. При превышении этого значения изображение либо выходит за пределы контейнера, либо масштабируется браузером, что ухудшает читаемость.
Не изменяйте значение dpi без соответствующего пересчёта ширины figsize, если требуется сохранить результат в пределах 980 пикселей. Например, при 150 DPI ширину следует уменьшить до 6,53 дюйма.
Раздел 6. Интерпретация результатов
Полностью автоматизированный пайплайн полезен только тогда, когда специалист понимает принципы интерпретации создаваемых им показателей. Ниже отдельно рассмотрены несколько распространённых ошибок.
Коэффициент Сортино и прибыль как основной критерий ранжирования: Чистая прибыль присутствует в схеме экспорта и в описании базового анализа, однако модуль Python ранжирует результаты InSample по коэффициенту Сортино. Это сделано намеренно. На коротких интервалах оптимизации стратегия может получить значительную абсолютную прибыль за счёт небольшого числа крупных выигрышных сделок, одновременно сохраняя крайне высокий риск неблагоприятных отклонений. Использование отрицательного отклонения в знаменателе коэффициента Сортино помогает отфильтровать такой шум. Отбор InSample по чистой прибыли гораздо сильнее подвержен риску выбора выброса, характерного только для конкретного рыночного режима.
Интерпретация большого значения profit_drop: Если модуль walk-forward сообщает, например, об отклонении прибыли на 72%, это не означает автоматической непригодности стратегии. Такой результат показывает, что окно InSample содержит условия, которые не повторяются в окне OutSample. Прежде чем заключать, что стратегия чрезмерно подогнана к истории, необходимо проверить, различаются ли эти окна во времени или структурно — например, не сменился ли трендовый период InSample боковым рынком в OutSample. Модуль предоставляет измерение, а специалист добавляет контекст.
Усреднение по таймфреймам в базовом модуле: Расчёт среднего значения по таймфреймам в baseline_analysis.py рассматривает M15, H1 и H4 как три статистически независимых наблюдения для каждого индикатора. Это разумное приближение, если периоды тестирования значительно длиннее самого крупного таймфрейма H4, однако при корреляции трёх таймфреймов из-за общего рыночного режима дисперсия оказывается заниженной. Для более глубокого исследования замените агрегирование mean() взвешенным средним, которое снижает вес младших таймфреймов в соответствии с их автокорреляцией со старшими.
Заключение
Описанная в статье архитектура разделяет три задачи, которые при ручном анализе MT5 обычно смешиваются: генерацию данных, аналитическую обработку и формирование текста аналитических выводов. MetaTrader 5 выполняет первую задачу, а стек Python автоматически решает вторую и третью.
Каркас экспорта MQL5 записывает структурированный CSV-файл, в котором указан этап каждого теста, сохранены все пользовательские счётчики и зафиксирована каждая конфигурация параметров. Базовый модуль считывает CSV и без ручной группировки или интерпретации графиков выполняет сравнение при одинаковых параметрах по инструментам и таймфреймам. Модуль walk-forward находит максимум InSample по коэффициенту Сортино, фиксирует его параметры, определяет точно соответствующую строку OutSample и представляет разницу результатов как однозначную оценку устойчивости.
Оба модуля создают визуализации публикационного качества с ограничением ширины 980 пикселей и формируют текст аналитических выводов, который можно непосредственно включить в структурированную статью. Для полного сравнительного анализа нового инструмента или таймфрейма достаточно обновить константы CSV и повторно запустить скрипты.
Пайплайн спроектирован так, чтобы строгий анализ был самым простым вариантом работы: не исключением, доступным лишь методологически аккуратным исследованиям, а автоматическим результатом каждого цикла разработки.
Программы, использованные в статье:
| # | Название | Тип | Описание |
|---|---|---|---|
| 1 | ExportFramework.mq5 | Демонстрационный советник | Компонент генерации данных MQL5. После каждого завершённого тестового прохода он записывает одну структурированную строку CSV, помечая её идентификатором Test_Phase: Baseline, InSample или OutSample. Пользовательские счётчики частоты ложных срабатываний и задержки при смене направления накапливаются во время прохода и записываются вместе со стандартной статистикой эффективности в OnDeinit(). |
| 2 | baseline_analysis.py | Скрипт Python | Считывает CSV базового этапа и группирует результаты по инструментам и индикаторам. Для каждого варианта индикатора рассчитывает средние значения по M15, H1 и H4, автоматически формирует текст аналитических выводов на основе агрегированных показателей и создаёт для каждого инструмента гистограмму с двумя панелями, сопоставляющую коэффициент Сортино с частотой ложных срабатываний. Оформленная сводная таблица экспортируется как изображение для непосредственного использования в публикации. |
| 3 | optimized_analysis.py | Скрипт Python | Считывает CSV этапа оптимизации и выполняет walk-forward-сопоставление с фиксированными параметрами. Для каждого индикатора определяет строку InSample с максимальным коэффициентом Сортино, извлекает точные значения её параметров и с помощью строгого равенства находит соответствующую строку OutSample. Разница результатов между двумя строками представляется как пропорциональное отклонение прибыли, а результаты визуализируются в виде гистограммы деградации. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22921
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Создание торговых систем с искусственным интеллектом на MQL5 (Часть 3): Переход к прокручиваемому интерфейсу с единым окном чата
От начального до среднего уровня: Очереди, списки и деревья (I)
Встраивание торговой дисциплины в код (Часть 7): Автоматизация защиты эквити с помощью логики управления
Династический алгоритм — Dynastic Optimization Algorithm (DOA)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
