English
preview
Разработка системы оценки портфеля из нескольких советников с учетом корреляции в MQL5

Разработка системы оценки портфеля из нескольких советников с учетом корреляции в MQL5

MetaTrader 5Торговые системы |
40 1
Cristian David Castillo Arrieta
Cristian David Castillo Arrieta

Введение

Вы создали несколько советников, каждый из которых показывает сильную кривую эквити в Тестере стратегий. Вы уверены в них и поэтому запускаете их все на одном счете. Затем наступает реальность: резкое движение доллара одновременно выбивает из игры половину ваших советников, а просадка оказывается в три раза больше, чем предсказывал любой отдельный бэктест. Что произошло?

Ответ обманчиво прост: советники были коррелированы, а их корреляция никогда не измерялась.

Многие разработчики MQL5 оптимизируют отдельные стратегии, но не оценивают их взаимодействие в портфеле. Это все равно что быть шеф-поваром, который в совершенстве владеет десятью рецептами, но никогда не задумывается, уместны ли они в одном меню. Портфель из пяти скальперов по золоту — не диверсификация, а концентрация, замаскированная под разнообразие.

В этой статье будет создан практический скрипт на MQL5, решающий эту проблему. В результате у вас будет готовый оценщик портфеля, который:

  • считывает кривые эквити из отчетов о бэктесте нескольких советников;
  • вычисляет матрицу корреляции Пирсона для каждой пары стратегий;
  • анализирует временное покрытие портфеля по часам торговли и дням недели;
  • вычисляет единую сводную оценку, показывающую, устойчив ли портфель или содержит опасно перекрывающиеся стратегии.
  • не требует внешних библиотек — не нужны ни Python, ни R, все работает нативно внутри MetaTrader 5.


Проблема — почему отдельные бэктесты искажают картину риска портфеля

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

  • EA_A торгует EURUSD на таймфрейме H1, используя стратегию возврата к среднему. Чистая прибыль: 45 000 долларов. Максимальная просадка: 8%.
  • EA_B торгует GBPUSD на таймфрейме H1, используя стратегию импульсного пробоя. Чистая прибыль: 38 000 долларов. Максимальная просадка: 7%.
  • EA_C торгует AUDUSD на таймфрейме H4, используя канальную стратегию. Чистая прибыль: 29 000 долларов. Максимальная просадка: 6%.

На бумаге это выглядит как диверсифицированный портфель. Три разные валютные пары, два разных таймфрейма и три разные стратегии. Можно было бы наивно ожидать, что совокупная просадка должна усредниться — возможно, до 7% в целом.

Но вот что упускает такой упрощенный расчет: в парах EURUSD, GBPUSD и AUDUSD доллар США выступает либо базовой, либо котируемой валютой. Когда Федеральная резервная система объявляет неожиданное решение по ставке, все три валютные пары резко движутся в одном направлении. "Диверсифицированный" портфель внезапно ведет себя как единая концентрированная ставка против доллара.

Именно здесь корреляционный анализ приобретает решающее значение. Если измерить дневную доходность EA_A и EA_B, можно обнаружить коэффициент корреляции Пирсона +0,72 — это означает, что они приносят прибыль и убытки примерно в одни и те же дни. Добавление EA_B в портфель с EA_A дает гораздо меньший эффект диверсификации, чем предполагают отдельные бэктесты.

Ключевой вывод таков: качество портфеля не сводится к сумме качеств отдельных стратегий. Оно определяется взаимодействием этих стратегий.


Что такое хороший портфель из нескольких советников

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

Критерий 1 — низкая корреляция между стратегиями

Коэффициент корреляции Пирсона измеряет линейную зависимость между двумя рядами доходности. Он изменяется от -1,0 (идеального движения в противоположных направлениях), до +1,0 (идеального сонаправленного движения). При формировании портфеля следует ориентироваться на следующие диапазоны:

  • |r| < 0,35: отлично — стратегии в значительной степени независимы;
  • 0,35 ≤ |r| < 0,60: приемлемо — наблюдается некоторое перекрытие, но ситуация остается контролируемой;
  • |r| ≥ 0,60: опасно — стратегии фактически дублируют рисковую экспозицию.

Следует учитывать, что отрицательные корреляции ценны, а не проблематичны. Советник с коэффициентом r = -0,40 относительно остальной части портфеля выступает естественным хеджем. Такой советник, как правило, приносит прибыль, когда у остальных возникает просадка. Именно к этому стремятся управляющие институциональными портфелями.

Критерий 2 — временное покрытие

Рынки ведут себя по-разному в разные часы и дни. Портфель, в котором все советники торгуют только с 14:00 до 18:00 по UTC со вторника по четверг, имеет опасные "слепые зоны". Идеальное покрытие означает: (1) в каждой основной сессии — азиатской, лондонской и нью-йоркской — активен как минимум один советник; (2) в каждый будний день есть хотя бы некоторая активность; и (3) торговля распределена по нескольким временным интервалам, а не сконцентрирована.

Критерий 3 — диверсификация по классам активов

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

Оценщик портфеля количественно оценит все три аспекта и объединит их в единый прикладной показатель.


Архитектура оценщика портфеля

Полное решение состоит из одного скрипта MQL5, разделенного на четыре логических модуля:

  1. загрузчик данных — считывает дневные P&L из CSV-файлов, по одному на каждого советника;
  2. модуль корреляции — вычисляет полную матрицу корреляции Пирсона размером N×N;
  3. анализатор покрытия — строит карту торговой активности по часам (0-23) и дням недели (пн-пт);
  4. функция оценки — объединяет все показатели во взвешенную сводную оценку.

Создадим каждый модуль шаг за шагом.

Модуль 1 — загрузчик данных

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

Ожидаемый формат CSV прост. Каждый файл содержит по одной строке на торговый день со следующими столбцами: Date, DailyPnL, TradeHour и TradeWeekday. Этот формат можно с минимальными усилиями сгенерировать на основе отчета Тестера стратегий.

Определим свойства скрипта и глобальные структуры данных:

#property script_show_inputs

//--- Input parameters
input string InpFileList = "EA_EUR.csv,EA_NDX.csv,EA_XAU.csv,EA_OIL.csv,EA_JPY.csv";
input string InpSeparator = ",";
input int    InpMinDays   = 60;

//--- Data structures
struct SEAData
  {
   string         name;
   double         dailyPnL[];
   int            tradeHours[];
   int            tradeDays[];
   int            totalDays;
   double         netProfit;
   double         maxDD;
  };

SEAData g_eaData[];
int     g_numEAs = 0;

А теперь — функция загрузки. Функция открывает CSV-файл, считывает каждую строку, накапливает дневные P&L и рассчитывает максимальную просадку как процент от пика эквити:

bool LoadEAData(string filename, SEAData &data)
  {
   data.name      = filename;
   data.totalDays = 0;
   data.netProfit = 0;
   data.maxDD     = 0;

   int handle = FileOpen(filename, FILE_READ|FILE_CSV|FILE_ANSI, ',');
   if(handle == INVALID_HANDLE)
     {
      PrintFormat("Error: Cannot open %s. Code: %d", filename, GetLastError());
      return false;
     }

   //--- Skip header line
   if(!FileIsEnding(handle))
     {
      while(!FileIsLineEnding(handle) && !FileIsEnding(handle))
         FileReadString(handle);
     }

   double tempPnL[];
   int    tempHours[];
   int    tempDays[];
   int    count = 0;

   while(!FileIsEnding(handle))
     {
      string dateStr = FileReadString(handle);
      if(dateStr == "") break;

      double pnl = FileReadNumber(handle);
      int hour = (int)FileReadNumber(handle);
      int wday = (int)FileReadNumber(handle);

      count++;
      ArrayResize(tempPnL, count, 100);
      ArrayResize(tempHours, count, 100);
      ArrayResize(tempDays, count, 100);

      tempPnL[count - 1]   = pnl;
      tempHours[count - 1] = hour;
      tempDays[count - 1]  = wday;

      data.netProfit += pnl;
     }

   FileClose(handle);

   if(count < InpMinDays)
     {
      PrintFormat("Warning: %s has only %d days (minimum: %d)", filename, count, InpMinDays);
      return false;
     }

   ArrayResize(data.dailyPnL, count);
   ArrayResize(data.tradeHours, count);
   ArrayResize(data.tradeDays, count);
   ArrayCopy(data.dailyPnL, tempPnL);
   ArrayCopy(data.tradeHours, tempHours);
   ArrayCopy(data.tradeDays, tempDays);
   data.totalDays = count;

   //--- Calculate max drawdown
   double peak = 0, equity = 0;
   for(int i = 0; i < count; i++)
     {
      equity += data.dailyPnL[i];
      if(equity > peak) peak = equity;
      
      double dd = (peak > 0) ? (peak - equity) / peak * 100 : 0;
      if(dd > data.maxDD) data.maxDD = dd;
     }

   PrintFormat("Loaded %s: %d days | Net: $%.2f | MaxDD: %.2f%%", filename, count, data.netProfit, data.maxDD);
   return true;
  }

Обратите внимание: максимальная просадка рассчитывается как процент от пика эквити. Это стандартный показатель, используемый сервисом "Сигналы MQL5" и профессиональными управляющими фондами. Просадка 10% означает, что эквити в какой-то момент опустилась на 10% ниже максимального зафиксированного уровня.

Модуль 2 — модуль корреляции

Это ядро оценщика портфеля. Коэффициент корреляции Пирсона между двумя рядами X и Y определяется как

r = Sum((Xi - Xmean)(Yi - Ymean)) / sqrt(Sum((Xi - Xmean)^2) * Sum((Yi - Ymean)^2))

Проще говоря, измеряется степень синхронности двух рядов доходности в диапазоне [-1, +1]. Реализация выглядит так:

double CalcPearsonCorrelation(const double &seriesA[], const double &seriesB[], int length)
  {
   if(length < 2) return 0.0;

   double meanA = 0, meanB = 0;
   for(int i = 0; i < length; i++)
     {
      meanA += seriesA[i];
      meanB += seriesB[i];
     }
   meanA /= length;
   meanB /= length;

   double covAB = 0, varA = 0, varB = 0;
   for(int i = 0; i < length; i++)
     {
      double dA = seriesA[i] - meanA;
      double dB = seriesB[i] - meanB;
      covAB += dA * dB;
      varA  += dA * dA;
      varB  += dB * dB;
     }

   double denom = MathSqrt(varA * varB);
   if(denom < 1e-10) return 0.0;

   return covAB / denom;
  }

Теперь строится полная матрица размером N×N. Именно здесь появляются ключевые выводы о портфеле:

double g_corrMatrix[];
int    g_matrixSize = 0;

void BuildCorrelationMatrix()
  {
   g_matrixSize = g_numEAs;
   ArrayResize(g_corrMatrix, g_matrixSize * g_matrixSize);
   ArrayInitialize(g_corrMatrix, 0.0);

   for(int i = 0; i < g_numEAs; i++)
     {
      for(int j = 0; j < g_numEAs; j++)
        {
         if(i == j)
           {
            g_corrMatrix[i * g_matrixSize + j] = 1.0;
            continue;
           }
         int len = MathMin(g_eaData[i].totalDays, g_eaData[j].totalDays);
         double r = CalcPearsonCorrelation(g_eaData[i].dailyPnL, g_eaData[j].dailyPnL, len);
         g_corrMatrix[i * g_matrixSize + j] = r;
        }
     }

   //--- Print the matrix
   PrintFormat("=== CORRELATION MATRIX (%d EAs) ===", g_numEAs);
   string header = "          ";
   for(int j = 0; j < g_numEAs; j++)
      header += StringFormat("%-10s", g_eaData[j].name);
   Print(header);

   for(int i = 0; i < g_numEAs; i++)
     {
      string row = StringFormat("%-10s", g_eaData[i].name);
      for(int j = 0; j < g_numEAs; j++)
        {
         double val = g_corrMatrix[i * g_matrixSize + j];
         row += StringFormat("%+.3f     ", val);
        }
      Print(row);
     }
  }

Если запустить код для портфеля из пяти советников, на вкладке "Эксперты" отобразится полная матрица корреляции, как показано на рис. 1:

Вкладка "Эксперты" — оценка портфеля

Рис. 1. Матрица корреляции и оценка портфеля на вкладке "Эксперты"

Эта матрица отражает зависимости между стратегиями. Наибольшая абсолютная корреляция составляет 0,312, между XAU и OIL. Это ниже порога 0,35, поэтому портфель сформирован грамотно. Отрицательные значения, например -0,201 для OIL и JPY, указывают на естественное хеджирование, что полезно.

Модуль 3 — анализатор покрытия

Портфель, торгующий только во время нью-йоркской сессии, уязвим к ночным ценовым разрывам. Анализатор покрытия строит карту активности советников по 24 часам суток и 5 торговым дням недели:

SCoverageResult AnalyzeCoverage()
  {
   SCoverageResult result;
   ArrayInitialize(result.hourMap, 0);
   ArrayInitialize(result.dayMap, 0);

   for(int ea = 0; ea < g_numEAs; ea++)
     {
      bool hourSeen[24];
      bool daySeen[5];
      ArrayInitialize(hourSeen, false);
      ArrayInitialize(daySeen, false);

      for(int i = 0; i < g_eaData[ea].totalDays; i++)
        {
         int h = g_eaData[ea].tradeHours[i];
         int d = g_eaData[ea].tradeDays[i];

         if(h >= 0 && h < 24 && !hourSeen[h])
           {
            hourSeen[h] = true;
            result.hourMap[h]++;
           }
         if(d >= 0 && d < 5 && !daySeen[d])
           {
            daySeen[d] = true;
            result.dayMap[d]++;
           }
        }
     }

   result.hoursActive = 0;
   result.daysActive  = 0;

   for(int h = 0; h < 24; h++)
      if(result.hourMap[h] > 0)
         result.hoursActive++;

   for(int d = 0; d < 5; d++)
      if(result.dayMap[d] > 0)
         result.daysActive++;

   double hourRatio = result.hoursActive / 24.0;
   double dayRatio  = result.daysActive / 5.0;
   result.coverageScore = hourRatio * 0.70 + dayRatio * 0.30;

   //--- Print coverage
   Print("\n=== COVERAGE ANALYSIS ===");
   PrintFormat("Hours covered: %d/24 | Days: %d/5", result.hoursActive, result.daysActive);

   string hourBar = "Hour Map:  ";
   for(int h = 0; h < 24; h++)
     {
      if(result.hourMap[h] == 0)
         hourBar += ".";
      else
         hourBar += IntegerToString(MathMin(result.hourMap[h], 9));
     }
   Print(hourBar);
   Print("           000000000011111111112222");
   Print("           012345678901234567890123");

   string dayNames[] = {"Mon", "Tue", "Wed", "Thu", "Fri"};
   for(int d = 0; d < 5; d++)
      PrintFormat("  %s: %d EAs active", dayNames[d], result.dayMap[d]);

   PrintFormat("Coverage Score: %.3f", result.coverageScore);
   return result;
  }

В результате на вкладке "Эксперты" создается наглядная текстовая карта активности. Каждая цифра в "карте часов" показывает, сколько советников активны в соответствующий час UTC. Точки обозначают пробелы — часы с нулевым покрытием. В устойчивом портфеле активность распределена по большинству часов, а число пробелов не превышает 1-2.

Анализ покрытия

Рис. 2. Анализ покрытия с картой по часам и разбивкой по дням недели на вкладке "Эксперты"

Здесь сразу видно, что 00:00 UTC — "слепая зона", а четверг — самый слабый день. Это практически применимый вывод: любой новый советник-кандидат в идеале должен усилить покрытие именно в этих пробелах.

Модуль 4 — совокупная оценка портфеля

Теперь объединим в единую оценку. Ключевое проектное решение — как назначить вес каждому аспекту. После тестирования различных конфигураций именно эти веса лучше всего предсказывают устойчивость на данных вне обучающей выборки:

  • оценка корреляции (50%) — наиболее важный фактор, штрафует портфели с высокой корреляцией между стратегиями;
  • оценка покрытия (25%) — поощряет портфели, охватывающие больше торговых часов и дней недели;
  • оценка диверсификации (25%) — поощряет портфели, охватывающие несколько классов активов.

struct SPortfolioScore
  {
   double            corrScore;
   double            coverScore;
   double            diversityScore;
   double            compositeScore;
   string            grade;
   int               highCorrPairs;
   double            avgAbsCorr;
  };

Функция CalcPortfolioScore() вычисляет оценки по всем трем аспектам и объединяет их с использованием настраиваемых весов:

SPortfolioScore CalcPortfolioScore(SCoverageResult &coverage)
  {
   SPortfolioScore score;

   //=== 1. Correlation Score ===
   double sumAbsCorr = 0;
   int    pairCount  = 0;
   score.highCorrPairs = 0;

   for(int i = 0; i < g_numEAs; i++)
     {
      for(int j = i + 1; j < g_numEAs; j++)
        {
         double r = g_corrMatrix[i * g_matrixSize + j];
         sumAbsCorr += MathAbs(r);
         pairCount++;
         if(MathAbs(r) >= InpCorrThresh)
            score.highCorrPairs++;
        }
     }

   score.avgAbsCorr = (pairCount > 0) ? (sumAbsCorr / pairCount) : 0;
   score.corrScore = MathMax(0, (1.0 - score.avgAbsCorr * 2.0)) * 100;

   //=== 2. Coverage Score ===
   score.coverScore = coverage.coverageScore * 100;

   //=== 3. Diversity Score ===
   string families[];
   int numFamilies = 0;

   for(int i = 0; i < g_numEAs; i++)
     {
      string family = DetectAssetFamily(g_eaData[i].name);
      bool found = false;
      for(int f = 0; f < numFamilies; f++)
         if(families[f] == family)
           { found = true; break; }

      if(!found)
        {
         numFamilies++;
         ArrayResize(families, numFamilies);
         families[numFamilies - 1] = family;
        }
     }

   score.diversityScore = MathMin(numFamilies * 20.0, 100.0);

   //=== 4. Composite ===
   score.compositeScore = score.corrScore * InpWeightCorr + 
                          score.coverScore * InpWeightCov + 
                          score.diversityScore * InpWeightDiv;

   //=== 5. Grade ===
   if(score.compositeScore >= 90) score.grade = "A+";
   else if(score.compositeScore >= 80) score.grade = "A";
   else if(score.compositeScore >= 70) score.grade = "B";
   else if(score.compositeScore >= 60) score.grade = "C";
   else if(score.compositeScore >= 50) score.grade = "D";
   else score.grade = "F";

   return score;
  }

Вспомогательная функция DetectAssetFamily() классифицирует каждого советника по типу инструмента, которым он торгует. Она ищет в имени файла советника известные идентификаторы валют, индексов, металлов, энергоносителей и акций:

string DetectAssetFamily(string eaName)
  {
   string upper = eaName;
   StringToUpper(upper);

   if(StringFind(upper, "EUR") >= 0 || StringFind(upper, "GBP") >= 0 ||
      StringFind(upper, "JPY") >= 0 || StringFind(upper, "AUD") >= 0 ||
      StringFind(upper, "CHF") >= 0 || StringFind(upper, "NZD") >= 0 ||
      StringFind(upper, "CAD") >= 0)
      return "FOREX";

   if(StringFind(upper, "NDX") >= 0 || StringFind(upper, "SPX") >= 0 ||
      StringFind(upper, "DAX") >= 0 || StringFind(upper, "NJ225") >= 0 ||
      StringFind(upper, "NAS") >= 0)
      return "INDICES";

   if(StringFind(upper, "XAU") >= 0 || StringFind(upper, "XAG") >= 0 ||
      StringFind(upper, "GOLD") >= 0)
      return "METALS";

   if(StringFind(upper, "OIL") >= 0 || StringFind(upper, "XTI") >= 0 ||
      StringFind(upper, "BRENT") >= 0 || StringFind(upper, "XNG") >= 0)
      return "ENERGY";

   if(StringFind(upper, "AMZN") >= 0 || StringFind(upper, "TSLA") >= 0 ||
      StringFind(upper, "NVDA") >= 0 || StringFind(upper, "AAPL") >= 0 ||
      StringFind(upper, "GOOGL") >= 0 || StringFind(upper, "XLV") >= 0 ||
      StringFind(upper, "XLP") >= 0)
      return "STOCKS";

   return "OTHER";
  }


Основной скрипт — объединяем все вместе

Функция OnStart() управляет всем рабочим процессом. Она разбирает список файлов и пытается загрузить каждый CSV-файл. Если файлы отсутствуют, генерируются тестовые данные, чтобы скрипт мог запускаться без ручной настройки.

void OnStart()
  {
   Print("========================================");
   Print("  PORTFOLIO SCORER v1.0");
   Print("  Multi-EA Correlation & Coverage Tool");
   Print("========================================");

   //--- Parse file list
   string fileList[];
   ushort sep = StringGetCharacter(InpSeparator, 0);
   int numFiles = StringSplit(InpFileList, sep, fileList);

   if(numFiles < 2)
     {
      Alert("Specify at least 2 EA CSV files.");
      return;
     }

   ArrayResize(g_eaData, numFiles);
   g_numEAs = 0;

   for(int i = 0; i < numFiles; i++)
     {
      string fname = fileList[i];
      StringTrimLeft(fname);
      StringTrimRight(fname);

      if(LoadEAData(fname, g_eaData[g_numEAs]))
         g_numEAs++;
     }

   if(g_numEAs < 2)
     {
      //--- Files not found: auto-generate
      Print(">> Not enough valid files. Auto-generating samples...");
      if(!EnsureSampleFiles(fileList, numFiles))
        {
         Alert("Could not generate sample files.");
         return;
        }

      //--- Retry loading with new files
      numFiles = ArraySize(fileList);
      ArrayResize(g_eaData, numFiles);
      g_numEAs = 0;

      for(int i = 0; i < numFiles; i++)
        {
         string fn = fileList[i];
         StringTrimLeft(fn);
         StringTrimRight(fn);
         if(LoadEAData(fn, g_eaData[g_numEAs]))
            g_numEAs++;
        }

      if(g_numEAs < 2)
        {
         Alert("Failed to load generated samples.");
         return;
        }
     }

   PrintFormat("\nSuccessfully loaded %d EAs.\n", g_numEAs);

   //--- Build correlation matrix
   BuildCorrelationMatrix();

   //--- Analyze coverage
   SCoverageResult coverage = AnalyzeCoverage();

   //--- Calculate score
   SPortfolioScore result = CalcPortfolioScore(coverage);

   //--- Final report
   Print("\n========================================");
   Print("  PORTFOLIO SCORE REPORT");
   Print("========================================");
   PrintFormat("  EAs in portfolio:    %d", g_numEAs);
   PrintFormat("  Avg |correlation|:   %.3f", result.avgAbsCorr);
   PrintFormat("  High-corr pairs:    %d", result.highCorrPairs);
   Print("----------------------------------------");
   PrintFormat("  Correlation Score:   %.1f / 100", result.corrScore);
   PrintFormat("  Coverage Score:      %.1f / 100", result.coverScore);
   PrintFormat("  Diversity Score:     %.1f / 100", result.diversityScore);
   Print("----------------------------------------");
   PrintFormat("  COMPOSITE SCORE:     %.1f / 100", result.compositeScore);
   PrintFormat("  GRADE:               %s", result.grade);
   Print("========================================");

   //--- Actionable recommendations
   Print("\n--- RECOMMENDATIONS ---");

   if(result.corrScore < 50)
      Print("!! CRITICAL: High inter-strategy correlation. Review the matrix.");

   if(result.highCorrPairs > 0)
      PrintFormat("   %d pair(s) exceed the %.2f correlation threshold.", result.highCorrPairs, InpCorrThresh);

   if(result.coverScore < 70)
      Print(">> TIP: Add EAs active in uncovered hours/days to reduce blind spots.");

   if(result.diversityScore < 60)
      Print(">> TIP: Diversify across more asset classes (forex, metals, indices).");

   if(result.compositeScore >= 80)
      Print("++ Portfolio is well-constructed. Maintain quality when adding new EAs.");

   Print("\n========================================");
   Print("  Scoring complete.");
   Print("========================================");
  }

В итоговом отчете об оценке портфеля на вкладке "Эксперты" отображаются оценки по всем трем аспектам, сводный результат и практические рекомендации:

Portfolio Score Report

Рис. 3. Итоговый отчет об оценке портфеля со сводной оценкой и рекомендациями



Подготовка данных — практический порядок действий

Чтобы использовать оценщик портфеля, вам понадобятся CSV-файлы с дневными P&L по каждому советнику. Вот как их создать:

  1. запустите бэктест в Тестере стратегий с нужными настройками;
  2. экспортируйте отчет через контекстное меню — щелкните правой кнопкой мыши по вкладке "Бэктест";
  3. обработайте сделки: сгруппируйте их по дате, подсчитайте суммарную прибыль за каждый день и зафиксируйте наиболее частые для торговли час и день недели.

Чтобы автоматизировать этот этап, используйте вспомогательную функцию, экспортирующую дневные P&L из обработчика OnTester() советника. Эта функция не входит в скрипт PortfolioScorer — ее нужно добавить в собственный советник и вызывать из OnTester().

//+------------------------------------------------------------------+
//| Export daily P&L to CSV from backtest                            |
//| Add this to YOUR EA and call from OnTester()                     |
//+------------------------------------------------------------------+
void ExportDailyPnL(string filename)
  {
   int handle = FileOpen(filename, FILE_WRITE|FILE_CSV|FILE_ANSI, ',');
   if(handle == INVALID_HANDLE) return;

   FileWriteString(handle, "Date,DailyPnL,TradeHour,TradeWeekday\n");

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

   datetime lastDate = 0;
   double   dayPnL   = 0;
   int      dayHour  = 12;

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

      double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT);
      datetime dealTime = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME);

      MqlDateTime dt;
      TimeToStruct(dealTime, dt);
      datetime dateOnly = StringToTime(StringFormat("%04d.%02d.%02d", dt.year, dt.mon, dt.day));

      if(dateOnly != lastDate && lastDate != 0)
        {
         MqlDateTime ld;
         TimeToStruct(lastDate, ld);
         FileWriteString(handle, StringFormat("%04d.%02d.%02d,%.2f,%d,%d\n", 
                                                  ld.year, ld.mon, ld.day, dayPnL, dayHour, ld.day_of_week - 1));
         dayPnL = 0;
        }

      lastDate = dateOnly;
      dayPnL  += profit;
      dayHour  = dt.hour;
     }

   //--- Write last day
   if(lastDate != 0)
     {
      MqlDateTime ld;
      TimeToStruct(lastDate, ld);
      FileWriteString(handle, StringFormat("%04d.%02d.%02d,%.2f,%d,%d\n", 
                                            ld.year, ld.mon, ld.day, dayPnL, dayHour, ld.day_of_week - 1));
     }

   FileClose(handle);
   PrintFormat("Exported daily P&L to %s", filename);
  }

Вызовите функцию ExportDailyPnL("EA_MyStrategy.csv") из функции OnTester() в советнике. После проведения бэктестов всех советников CSV-файлы будут готовы в папке MQL5\Files терминала.


Интерпретация результатов и принятие решений

Оценщик портфеля выдает оценку от A+ до F. Однако истинная ценность заключается в оценках по отдельным аспектам и в том, как они помогают принять следующее решение:

Сценарий 1 — высокая сводная оценка (80+), оценка A

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

Сценарий 2 — низкая оценка корреляции, менее 50

Главная проблема — избыточность. Просмотрите матрицу корреляции, чтобы найти пару с наибольшим значением |r|. Возможны три варианта: удалить один из коррелирующих советников, заменить его советником, работающим с другим классом активов, или скорректировать размер лота, чтобы уменьшить вес избыточной стратегии.

Сценарий 3 — низкая оценка покрытия, менее 70

Во временном покрытии портфеля есть слепые зоны. Проверьте карту активности по часам на наличие пробелов. Если в часы 0-7 UTC нет торгов, рассмотрите возможность добавить советник, торгующий во время азиатской сессии. Если покрытие в четверг слабое, ищите стратегии, генерирующие сигналы именно в четверг.

Сценарий 4 — низкая оценка диверсификации, ниже 60

Портфель сконцентрирован в слишком малом числе классов активов. Если все советники торгуют на Forex, рассмотрите возможность добавить стратегию по золоту, индексам или акциям. Даже небольшое выделение капитала на другой рынок может значительно повысить устойчивость портфеля.

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


Расширение методики

Представленный здесь оценщик портфеля — основа для дальнейшего развития. Несколько улучшений могут сделать его еще эффективнее:

  • Скользящие окна корреляции: вместо одного значения корреляции за весь бэктест рассчитывайте корреляции на скользящих 60-дневных окнах. Это показывает, стабильны ли корреляции или меняются со временем — нестабильная корреляция является скрытым фактором риска.
  • Моделирование методом Монте-Карло: случайным образом перемешивайте последовательности дневных P&L и пересчитывайте оценку тысячи раз. В результате получается доверительный интервал для сводной оценки.
  • Оптимизация размера лота: зная матрицу корреляции, можно рассчитать на ее основе оптимальные размеры лотов, минимизирующие дисперсию портфеля. По сути, это оптимизация Марковица по модели "среднее — дисперсия", примененная к торговым стратегиям вместо акций.
  • Панель мониторинга в реальном времени: преобразуйте скрипт в советник, работающий на графике в реальном времени, непрерывно отслеживающий корреляцию между работающими стратегиями и выдающий алерты при резком превышении корреляцией порогового значения.


Заключение

Создание прибыльных отдельных советников — лишь половина дела. Другая, и, пожалуй, более важная половина — формирование портфеля, в котором советники дополняют, а не дублируют друг друга.

В этой статье создан полнофункциональный оценщик портфеля на MQL5, количественно оценивающий качество портфеля по трем аспектам: межстратегической корреляции, временному покрытию и диверсификации по классам активов. Инструмент считывает дневные P&L из CSV-файлов бэктестов, вычисляет полную матрицу корреляции Пирсона, строит карту торговой активности по часам и дням недели и выдает сводную оценку, сразу показывающую качество портфеля.

Основные выводы для практиков:

  1. Перед объединением советников измерьте их корреляцию. Портфель из пяти коррелирующих стратегий не становится в пять раз надежнее — он может оказаться в пять раз уязвимее.
  2. Пробелы в покрытии позволяют принимать практические решения. Используйте карты активности по часам и дням недели, чтобы точно определить, где портфелю требуется усиление.
  3. Диверсификация по классам активов обязательна для серьезных алгоритмических портфелей. Секторальная концентрация — "тихий убийца" систем с несколькими советниками.
  4. Наилучший советник для добавления — тот, который в наибольшей степени повышает оценку портфеля, а не обязательно тот, у которого самый высокий отдельный фактор прибыли.

Исходный код оценщика портфеля доступен в MQL5 CodeBase: Оценщик портфеля — анализатор корреляции и покрытия для нескольких советников. Скачайте его, адаптируйте к своим стратегиям и начните оценивать то, что большинство трейдеров игнорирует: качество самого портфеля, а не только качество его составляющих.

Переход от "создания советников" к "проектированию портфелей" — это переход от мышления частного трейдера к институциональному подходу. Этот скрипт — ваш первый шаг в этом направлении.

В таблице ниже перечислены все файлы исходного кода, прилагаемые к статье.


Имя файла


Описание


PortfolioScorer.mq5

Основной скрипт, объединяющий все модули. Он обрабатывает данные, рассчитывает сводную оценку и формирует итоговый отчет о состоянии портфеля на вкладке "Эксперты".
Data_Loader.mq5
Модуль 1: считывает дневные P&L из предоставленных CSV-файлов, рассчитывает максимальную просадку и структурирует данные для анализа.
Correlation_Engine.mq5
Модуль 2: вычисляет коэффициент корреляции Пирсона между всеми парами стратегий и строит матрицу корреляции размером N×N.
Coverage_Analyzer.mq5
Модуль 3: анализирует и строит карту торговой активности по часам (0-23) и дням недели (пн-пт), чтобы выявить временные слепые зоны в портфеле.
Utility_ExportDailyPnL.mq5
Автономный служебный скрипт, который можно легко подключить к OnTester() собственных советников для экспорта дневных P&L в CSV-формат для бэктестов.


Список источников
  1. MetaQuotes, "Справочник MQL5 — Файловые операции", Документация MQL5: Файлы
  2. MetaQuotes, "Справочник MQL5 — Математические функции", Документация MQL5: Математика
  3. MetaQuotes, "Свойства сделок", Документация MQL5: Свойства сделок
  4. Исходный код оценщика портфеля, MQL5 CodeBase : Portfolio Scorer — Multi-EA Correlation and Coverage Analyzer

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

Прикрепленные файлы |
PortfolioScorer.mq5 (49.94 KB)
Data_Loader.mq5 (6.13 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Solomon Anietie Sunday
Solomon Anietie Sunday | 28 мая 2026 в 07:40
Эта статья будет чрезвычайно полезна всем, кто только собирается создать портфолио или уже занимается его составлением. Очень стратегический материал. Огромное спасибо автору за время и усилия, затраченные на подготовку этой статьи.
От новичка до эксперта: Автоматизация внутридневных стратегий От новичка до эксперта: Автоматизация внутридневных стратегий
Мы переводим идею ретеста EMA(50) в советник для внутридневной торговли, основанный на поведении цены. В исследовании формализуются трендовый уклон, взаимодействие с EMA (проколоть/пробить и закрыться), подтверждение реакции и дополнительные фильтры, после чего они реализуются в MQL5 с использованием модульных функций и дескрипторов с безопасным управлением ресурсами. Визуальное тестирование в Тестере стратегий позволяет проверить корректность сигналов. В результате получается понятный шаблон для кодирования дискреционных отскоков.
Использование Экономического календаря MQL5 для фильтрации новостей (Часть 3): Сохранение работы при перезапусках терминала во время новостного окна Использование Экономического календаря MQL5 для фильтрации новостей (Часть 3): Сохранение работы при перезапусках терминала во время новостного окна
В статье представлена устойчивая к перезапуску модель хранения состояния для удаления стопов на время новостей. Состояние приостановки и исходные значения SL/TP для каждой позиции записываются в глобальные переменные терминала, считываются и реконструируются в OnInit() и удаляются после восстановления стопов. Это позволяет советнику продолжить активное окно приостановки после перекомпиляции или перезапуска и восстановить стопы только после окончания новостного окна.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Разработка научно обоснованного сеточного советника в MQL5: Почему большинство сеточных советников терпят неудачу и что доказал Таранто Разработка научно обоснованного сеточного советника в MQL5: Почему большинство сеточных советников терпят неудачу и что доказал Таранто
В этой статье реализуется сеточный советник, адаптирующийся к режиму рынка и основанный на докторском исследовании Альдо Таранто. Представлен сеточный советник, адаптирующийся к режиму рынка и ограничивающий риск с помощью перезапускаемых циклов и защитных механизмов на основе эквити. Мы объясняем, почему наивные сетки терпят неудачу (из-за роста дисперсии и почти верного разорения), выводим выводим формулу убытка для расчета текущей экспозиции и реализуем фильтрацию запуска с учетом режима рынка, динамический шаг сетки на основе ATR и аварийный выключатель, работающий в реальном времени. Читатели получат математические инструменты и пригодные для рабочей эксплуатации подходы, необходимые для безопасной разработки, тестирования и эксплуатации сеточной стратегии с ограничениями.