preview
Building a Correlation-Aware Multi-EA Portfolio Scorer in MQL5

Building a Correlation-Aware Multi-EA Portfolio Scorer in MQL5

MetaTrader 5Trading systems |
462 0
Cristian David Castillo Arrieta
Cristian David Castillo Arrieta

Introduction

You have built multiple Expert Advisors, each with a strong equity curve in the Strategy Tester. You feel confident, so you load them all onto a single account. Then reality hits: a strong dollar move wipes out half your EAs simultaneously, and the drawdown is three times what any individual backtest predicted. What happened?

The answer is deceptively simple: your EAs were correlated, and you never measured it.

Many MQL5 developers optimize individual strategies but do not evaluate how the strategies interact in a portfolio. This is the equivalent of a chef who masters ten recipes but never considers whether they belong on the same menu. A portfolio of five gold scalpers is not diversification—it is concentration disguised as variety.

In this article, we will build a practical MQL5 script that solves this problem. By the end, you will have a working portfolio scorer that:

  • Reads equity curves from multiple EA backtest reports.
  • Calculates a Pearson correlation matrix between every pair of strategies.
  • Analyzes trading hour and day-of-week coverage across the portfolio.
  • Produces a single composite score that tells you whether your portfolio is robust or dangerously overlapping.
  • No external libraries are needed. No Python, no R. Everything runs natively inside MetaTrader 5.


The Problem: Why Individual Backtests Lie About Portfolio Risk

Let us start with a concrete scenario that every multi-EA trader recognizes. Suppose you have optimized three Expert Advisors:

  • EA_A trades EURUSD on H1 using a mean-reversion approach. Net profit: $45,000. Max drawdown: 8%.
  • EA_B trades GBPUSD on H1 using a momentum breakout. Net profit: $38,000. Max drawdown: 7%.
  • EA_C trades AUDUSD on H4 using a channel strategy. Net profit: $29,000. Max drawdown: 6%.

On paper, this looks like a diversified portfolio. Three different pairs, two different timeframes, and three different strategies. The naive expectation is that the combined drawdown should average out—perhaps 7% total.

But here is what the naive calculation misses: EURUSD, GBPUSD, and AUDUSD all share USD as either the base or quote currency. When the Federal Reserve announces an unexpected rate decision, all three pairs move violently in the same direction. Your "diversified" portfolio suddenly behaves like a single concentrated bet against the dollar.

This is where correlation analysis becomes essential. If we measure the daily returns of EA_A and EA_B, we might find a Pearson coefficient of +0.72—meaning they win and lose on roughly the same days. Adding EA_B to a portfolio that already contains EA_A provides far less diversification benefit than the individual backtests suggest.

The critical insight is this: portfolio quality is not the sum of individual strategy quality. It is a function of how those strategies interact.


Defining a Good Multi-EA Portfolio

Before we write any code, we need to define what "good" means in a portfolio context. Through extensive testing across multiple asset classes, three dimensions emerge as the most predictive of real-world portfolio robustness:

Dimension 1: Low Inter-Strategy Correlation

The Pearson correlation coefficient measures linear dependency between two series of returns. It ranges from -1.0 (perfect inverse movement) to +1.0 (perfect co-movement). For portfolio construction, we want the following ranges:

  • |r| < 0.35: Excellent—strategies are largely independent.
  • 0.35 ≤ |r| < 0.60: Acceptable—some overlap, but manageable.
  • |r| ≥ 0.60: Dangerous—strategies are effectively duplicating risk exposure.

Note that negative correlations are valuable, not problematic. An EA with r = -0.40 relative to the rest of the portfolio acts as a natural hedge. It tends to profit when others draw down. This is precisely what institutional portfolio managers seek.

Dimension 2: Temporal Coverage

Markets behave differently at different hours and on different days. A portfolio where all EAs trade exclusively between 14:00 and 18:00 UTC on Tuesday through Thursday has dangerous blind spots. Ideal coverage means (1) at least one EA is active in each major session (Asian, London, and New York); (2) every weekday has at least some activity; and (3) trading is spread across multiple time windows rather than concentrated.

Dimension 3: Asset Class Diversity

Even perfectly decorrelated strategies on the same asset class can be disrupted by a single sector shock. True robustness requires exposure across different market categories: forex majors, indices, commodities, equities, and possibly bonds.

Our portfolio scorer will quantify all three dimensions and combine them into a single actionable metric.


Architecture of the Portfolio Scorer

The complete solution consists of a single MQL5 script divided into four logical modules:

  1. Data Loader: Reads daily P&L data from CSV files (one per EA).
  2. Correlation Engine: Computes the full NxN Pearson correlation matrix.
  3. Coverage Analyzer: Maps trading activity by hour (0-23) and weekdays (Mon–Fri).
  4. Scoring Function: Combines all metrics into a weighted composite score.

Let us build each module step by step.

Module 1: The Data Loader

First, we need a data structure to hold each EA's daily returns. We will use a simple struct and read from CSV files that the user prepares from backtest reports.

The CSV format we expect is straightforward. Each file has one row per trading day with columns: Date, DailyPnL, TradeHour, and TradeWeekday. This format can be generated from the Strategy Tester report with minimal effort.

We define the script properties and global data structures:

#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;

Now, the loading function. It opens a CSV file, reads each line, accumulates the daily P&L values, and calculates the maximum drawdown as a percentage of the equity peak:

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;
  }

Notice how we calculate the maximum drawdown as a percentage of the equity peak. This is the standard metric used by MQL5 Signals and professional fund managers. A 10% drawdown means the equity once fell 10% from its highest recorded point.

Module 2: The Correlation Engine

This is the heart of our scorer. The Pearson correlation coefficient between two series X and Y is defined as

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

In plain language, we measure how much two return series move together, normalized to the range [-1, +1]. Here is the implementation:

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;
  }

Now we build the full NxN matrix. This is where the real portfolio insight emerges:

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);
     }
  }

When you run this on a portfolio of 5 EAs, the Experts tab will display the full correlation matrix as shown in Fig. 1:

Tab Expert Portfolio Score

Fig. 1. Correlation matrix and portfolio score output in the Experts tab

This matrix summarizes inter-strategy dependencies. The highest absolute correlation is 0.312 (between XAU and OIL), which is below our 0.35 threshold—this is a well-constructed portfolio. Negative values (like OIL vs. JPY at -0.201) indicate natural hedging relationships, which are beneficial.

Module 3: Coverage Analyzer

A portfolio that only trades during the New York session is vulnerable to overnight gaps. The Coverage Analyzer maps when each EA is active across the 24-hour cycle and the 5-day trading week:

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;
  }

The output creates a visual, text-based activity map in the Experts tab. Each digit in the "Hour Map" shows how many EAs are active during that UTC hour. Dots represent gaps—hours with zero coverage. A healthy portfolio shows activity spread across most hours with no more than 1-2 gaps.

Coverage Analysis

Fig. 2. Coverage analysis showing hour map and weekday breakdown in the Experts tab

Here we can immediately see that hour 00 UTC is a blind spot and Thursday is the weakest day. This is an actionable insight: any new EA candidate should ideally strengthen these specific gaps.

Module 4: The Composite Portfolio Score

Now we combine everything into a single score. The key design decision is how to weight each dimension. After testing various configurations, these weights proved most predictive of out-of-sample robustness:

  • Correlation Score (50%): the most important factor. Penalizes portfolios with high inter-strategy correlation.
  • Coverage Score (25%): rewards portfolios that cover more trading hours and weekdays.
  • Diversity Score (25%): rewards portfolios that span multiple asset classes.

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

The CalcPortfolioScore() function computes all three dimension scores and combines them using the configurable weights:

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;
  }

The DetectAssetFamily() helper classifies each EA by the type of instrument it trades. It scans the EA filename for known currency, index, metal, energy, and stock identifiers:

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";
  }


The Main Script: Putting It All Together

The OnStart() function orchestrates the entire workflow. It parses the file list and attempts to load each CSV. If files are missing, it generates sample data so the script can run without manual setup.

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("========================================");
  }

The final portfolio score report in the Experts tab shows all three dimension scores, the composite result, and actionable recommendations:

Portfolio Score Report

Fig. 3. Final portfolio score report with composite grade and recommendations



Preparing Your Data: A Practical Workflow

To use the portfolio scorer, you need CSV files with daily P&L data for each EA. Here is how to generate them:

  1. Run your backtest in the Strategy Tester with the desired settings.
  2. Export the report using the context menu (right-click on the Backtest tab).
  3. Process the deals: Group them by date, sum the profit per day, and record the most frequent trade hour and weekday.

To automate this step, use a utility function that exports daily P&L from the EA's OnTester() event. This function is not part of the PortfolioScorer script—you add it to your own EA and call it from 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);
  }

Call ExportDailyPnL("EA_MyStrategy.csv") from your EA's OnTester() function. After running backtests for all your EAs, the CSV files will be ready in the terminal's MQL5\Files folder.


Interpreting Results and Making Decisions

The portfolio scorer outputs a grade from A+ to F. But the real value lies in the individual dimension scores and how they guide your next decision:

Scenario 1: High composite score (80+), Grade A

Your portfolio is well-constructed. The EAs are decorrelated, coverage is broad, and you span multiple asset classes. Focus on maintaining this quality: when adding a new EA, re-run the scorer to verify it does not degrade the score.

Scenario 2: Low Correlation Score (below 50)

Your biggest problem is redundancy. Look at the correlation matrix to find the pair with the highest |r|. You have three options: remove one of the correlated EAs, replace it with an EA on a different asset class, or adjust lot sizing to reduce the weight of the redundant strategy.

Scenario 3: Low Coverage Score (below 70)

Your portfolio has temporal blind spots. Check the hour map for gaps. If hours 0-7 UTC are empty, consider adding an EA that trades during the Asian session. If Thursday is weak, look for strategies that generate Thursday signals.

Scenario 4: Low Diversity Score (below 60)

You are concentrated in too few asset classes. If all your EAs trade forex, consider adding a gold, index, or equity strategy. Even a modest allocation to a different market can dramatically improve portfolio resilience.

A practical implication is that the best EA to add may be the one that improves the portfolio score, not the one with the highest standalone profit. An EA with modest returns but negative correlation to your existing portfolio may contribute more to long-term stability than a high-profit EA that moves in lockstep with everything else.


Extending the Framework

The portfolio scorer presented here is a foundation. Several enhancements can make it even more powerful:

  • Rolling Correlation Windows: Instead of a single correlation value over the entire backtest, calculate correlations over rolling 60-day windows. This reveals whether correlations are stable or shift over time—an unstable correlation is a hidden risk factor.
  • Monte Carlo Simulation: Randomly shuffle the daily P&L sequences and recalculate the score thousands of times. This produces a confidence interval for your composite score.
  • Lot Size Optimization: Once you know the correlation matrix, you can use it to calculate optimal lot sizes that minimize portfolio variance. This is essentially Markowitz mean-variance optimization applied to trading strategies instead of stocks.
  • Real-Time Dashboard: Convert the script into an Expert Advisor that runs on a live chart, continuously monitoring the correlation between your running strategies and alerting you when correlations spike above the threshold.


Conclusion

Building profitable individual EAs is only half the job. The other half—and arguably the more important one—is constructing a portfolio where those EAs complement rather than duplicate each other.

In this article, we built a complete portfolio scorer in MQL5 that quantifies portfolio quality across three dimensions: inter-strategy correlation, temporal coverage, and asset class diversity. The tool reads daily P&L data from backtest CSV files, computes a full Pearson correlation matrix, maps trading activity across hours and weekdays, and produces a composite grade that instantly communicates portfolio health.

The key takeaways for practitioners are:

  1. Measure correlation before combining EAs. A portfolio of five correlated strategies is not five times safer—it may be five times more fragile.
  2. Coverage gaps are actionable. Use the hour and weekday maps to identify exactly where your portfolio needs reinforcement.
  3. Diversity across asset classes is non-negotiable for serious algo portfolios. Sector concentration is the silent killer of multi-EA systems.
  4. The best EA to add is the one that improves the portfolio score the most, not necessarily the one with the highest individual profit factor.

The source code of the portfolio scorer is available in the MQL5 CodeBase: Portfolio Scorer—Multi-EA Correlation and Coverage Analyzer. Download it, adapt it to your own strategies, and start measuring what most traders ignore: the quality of the portfolio itself, not just the quality of its parts.

The shift from "building EAs" to "engineering portfolios" is the transition from retail to institutional thinking. This script is your first step in that direction.

The following table describes all the source code files that accompany the article.


File Name


Description


PortfolioScorer.mq5

The main script that integrates all modules. It parses the data, computes the composite score, and generates the final portfolio health report in the Experts tab.
Data_Loader.mq5
Module 1: Reads daily P&L data from the provided CSV files, calculates the maximum drawdown, and structures the data for analysis.
Correlation_Engine.mq5
Module 2: Computes the Pearson correlation coefficient between all pairs of strategies and builds the NxN correlation matrix.
Coverage_Analyzer.mq5
Module 3: Analyzes and maps the trading activity by hour (0-23) and weekday (Mon-Fri) to identify temporal blind spots in the portfolio.
Utility_ExportDailyPnL.mq5
A standalone utility script designed to be included in your own Expert Advisors' OnTester() function easily to export daily P&L data into CSV format for backtesting.


References:
  1. MetaQuotes, "MQL5 Reference - File Functions," MQL5 Documentation: Files
  2. MetaQuotes, "MQL5 Reference - Math Functions," MQL5 Documentation: Math
  3. MetaQuotes, "History Deals Properties," MQL5 Documentation: Deal Properties 
  4. Portfolio Scorer source code, MQL5 CodeBase: Portfolio Scorer—Multi-EA Correlation and Coverage Analyzer
Features of Custom Indicators Creation Features of Custom Indicators Creation
Creation of Custom Indicators in the MetaTrader trading system has a number of features.
Predicting Renko Bars with CatBoost AI Predicting Renko Bars with CatBoost AI
How to use Renko bars with AI? Let's look at Renko trading on Forex with forecast accuracy of up to 59.27%. We will explore the benefits of Renko bars for filtering market noise, learn why volume is more important than price patterns, and how to set the optimal Renko block size for EURUSD. This is a step-by-step guide on integrating CatBoost, Python, and MetaTrader 5 to create your own Renko Forex forecasting system. It is ideal for traders looking to go beyond traditional technical analysis.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
One-Dimensional Singular Spectrum Analysis One-Dimensional Singular Spectrum Analysis
The article examines the theoretical and practical aspects of the singular spectrum analysis (SSA) method, which is an efficient method of time series analysis that allows one to represent the complex structure of a series as a decomposition into simple components, such as trend, seasonal (periodic) fluctuations and noise.