preview
Building Your Personal Expert Advisor (Part 6): Risk Management V — Portfolio and Correlated Risk

Building Your Personal Expert Advisor (Part 6): Risk Management V — Portfolio and Correlated Risk

MetaTrader 5Examples |
110 0
Solomon Anietie Sunday
Solomon Anietie Sunday

Contents

  1. Introduction
  2. Section 1: Building PortfolioRisk.mqh
  3. Section 2: Using the Library in the Series EA
  4. Pearson Correlation and Why It Is Not Here
  5. What This Part Does Not Do
  6. Conclusion


Introduction

The previous part taught the EA to see its own basket. Our EA can now measure its own basket. It treats every position it holds on its symbol as one unit, caps what that unit may lose, caps the margin it ties up, and reports how far against it things went at their worst. It still sees nothing else. Run the same EA on three charts, and each copy believes it is the only thing in the account.

The broker has no such illusion. Margin, margin level, and a margin call are account-wide facts. An account can be emptied by the sum of positions that were each individually well managed. This article builds a library that measures the account rather than the strategy and an EA that consults it before every trade.


Problem: Every EA Believes It Is Alone

A trader holds three positions: long EURUSD, long GBPUSD, and short USDJPY. Asked about concentration, the answer feels like "three different pairs, three different bets."

It is one bet. All three are short the dollar. Every pair splits into two currency legs. Add the legs up, and the diversification disappears.

Currency decomposition

Fig. 1. Three positions, one bet. Figures illustrate the arithmetic the library performs. They are not results from a test run.

A long EURUSD is long euros and short dollars. A long GBPUSD is long pounds and short dollars. A short USDJPY is short dollars and long yen. A dollar rally takes money out of all three on the same day. Every control we have built passes this. The per-trade sizing is correct on each one. The basket controls look at a single symbol and see nothing unusual. None of them were ever given the currency the three trades have in common.

Series three gates

Fig. 2. Three gates, three different questions.

A trade has to pass all three. Each one can see something the others cannot. Passing the first two says nothing about the third. Margin and a margin call are account-wide facts.


Why Portfolio Risk Belongs in a Header File

A portfolio limit enforced privately inside one EA is not a portfolio limit. Suppose our EA caps total account margin at forty percent. A second EA on the same account has never heard of that rule. It opens whatever its own logic allows, the account sails past forty percent, and our ceiling was decoration the whole time. It was never measuring the account. Furthermore, it was measuring its own opinion of the account. The control matters only when every EA measures the same account view and follows the same limits. That is what makes a shared file the right answer here rather than a matter of taste. So the measurement goes into PortfolioRisk.mqh, and any EA that wants it includes the file.


Scope of This Article

We are adding one header file and wiring it into the existing EA. The signal logic, the sizing, the basket controls, and the execution pipeline are all unchanged. What the library adds:

  • A scope setting that decides whether "the portfolio" means the whole account or a named list of magic numbers.
  • One scan that reports positions, pending orders, distinct symbols, volume, floating results, and margin.
  • A breakdown of exposure by currency so correlated positions can be recognized without a price history.
  • Six limits are checked before a trade opens.

What the EA adds: eleven inputs, a configuration block at startup, one function to parse a magic-number list, and one call in the validation stage. The first half below covers the header file on its own. The second half covers the EA that uses it.


Section 1: Building PortfolioRisk.mqh

Choosing What Counts as the Portfolio

The first decision is not technical. It is a question about how you run your account, and there are two defensible answers.

//+------------------------------------------------------------------+
//| What counts as "the portfolio".                                  |
//+------------------------------------------------------------------+
enum ENUM_PORTFOLIO_SCOPE
  {
   PORTFOLIO_ACCOUNT_WIDE,     // Every position on the account
   PORTFOLIO_MAGIC_FILTERED    // Only registered magic numbers
  };

Account-wide counts every position regardless of which EA opened it or whether an EA opened it at all. It sees manual trades, the other strategy you forgot was running, and the position you opened on your phone. This is the default.

Magic-filtered counts only registered magic numbers. It gives cleaner attribution when several strategies are deliberately budgeted apart, at the cost of being blind to everything it does not own. Neither is wrong, which is why it is an enumeration. When a decision is a philosophy choice rather than a correctness one, an enumeration puts the trade-off in front of the user.


The Two Structures

The library reports through two structures. The first is the account picture:

//+------------------------------------------------------------------+
//| A snapshot of total account exposure                             |
//+------------------------------------------------------------------+
struct SPortfolioState
  {
   int               positionCount;
   int               pendingCount;
   int               symbolCount;
   double            totalVolume;
   double            floatingPL;
   double            usedMargin;
   double            freeMargin;
   double            marginLevel;
   double            equity;
   double            balance;
  };

The second describes one currency:

//+------------------------------------------------------------------+
//| Net directional exposure to a single currency                    |
//+------------------------------------------------------------------+
struct SCurrencyExposure
  {
   string            currency;
   double            netLots;        // Signed: positive = net long this currency
   double            grossLots;      // Unsigned total, regardless of direction
   int               positionCount;
  };

Net and gross answer different questions. Net says which way you lean and by how much. Gross says how much is riding on that currency in total, including exposure that currently offsets itself. A portfolio with a net of zero and a gross of eight lots is not flat in any useful sense. It is heavily involved and temporarily balanced.


The Class Interface

//+------------------------------------------------------------------+
//| CPortfolioRisk                                                   |
//+------------------------------------------------------------------+
class CPortfolioRisk
  {
private:
   ENUM_PORTFOLIO_SCOPE m_scope;
   long              m_magics[];

   //--- Limits. Zero or negative disables the individual check.
   int               m_maxTotalPositions;
   int               m_maxPositionsPerSymbol;
   int               m_maxSymbols;
   double            m_maxMarginPercent;
   double            m_maxLossPercent;
   double            m_maxNetCurrencyLots;

   bool              IsTracked(const long magic) const;
   int               FindCurrency(const SCurrencyExposure &list[], const string currency) const;
   void              AccumulateCurrency(SCurrencyExposure &list[], const string currency,
                                        const double signedLots, const bool countPosition);

public:
                     CPortfolioRisk(void);

   //--- Configuration
   void              SetScope(const ENUM_PORTFOLIO_SCOPE scope) { m_scope = scope; }
   void              RegisterMagic(const long magic);
   void              SetPositionLimits(const int maxTotal, const int maxPerSymbol, const int maxSymbols);
   void              SetMarginLimit(const double maxMarginPercent)   { m_maxMarginPercent = maxMarginPercent; }
   void              SetLossLimit(const double maxLossPercent)       { m_maxLossPercent = maxLossPercent; }
   void              SetCurrencyLimit(const double maxNetLots)       { m_maxNetCurrencyLots = maxNetLots; }

   //--- Measurement
   bool              Scan(SPortfolioState &state);
   int               GetCurrencyExposure(SCurrencyExposure &exposure[]);
   double            NetLotsForCurrency(const string currency);
   void              PrintSnapshot(void);

   //--- The gate
   bool              CanOpenPosition(const string symbol, const ENUM_ORDER_TYPE orderType,
                                     const double volume, const double price, string &reason);
  };

Three public groups: settings, measurement, and one question asked before a trade. Every limit starts at zero in the constructor, and zero disables that check. A shared file that is included but never configured, therefore, changes nothing. Code other people include should not acquire opinions until they are asked to.


Scan(): Reading the Account in One Pass

Scan() walks positions and orders once and fills the state structure. One detail decides how it works. Margin is obtained two different ways depending on scope. Under account-wide scope, the terminal already knows the exact figure, so it is read directly. Under magic-filtered scope, no such figure exists, because MetaTrader publishes no per-position margin, so each position has to be priced on its own and the total becomes an approximation. As a result, the account-wide scope both sees everything and measures margin exactly. Narrowing the view costs accuracy as well as coverage.

//+------------------------------------------------------------------+
//| Take a snapshot of total exposure.                               |
//+------------------------------------------------------------------+
bool CPortfolioRisk::Scan(SPortfolioState &state)
  {
   ZeroMemory(state);

   state.equity     = AccountInfoDouble(ACCOUNT_EQUITY);
   state.balance    = AccountInfoDouble(ACCOUNT_BALANCE);
   state.freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   state.marginLevel = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);

   string seenSymbols[];
   ArrayResize(seenSymbols, 0);

   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(!PositionSelectByTicket(ticket))
         continue;
      if(!IsTracked(PositionGetInteger(POSITION_MAGIC)))
         continue;

      string symbol = PositionGetString(POSITION_SYMBOL);
      double volume = PositionGetDouble(POSITION_VOLUME);

      state.positionCount++;
      state.totalVolume += volume;
      state.floatingPL  += PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);

      //--- Distinct symbol tally
      bool known = false;
      int seen = ArraySize(seenSymbols);
      for(int s = 0; s < seen; s++)
         if(seenSymbols[s] == symbol)
           {
            known = true;
            break;
           }
      if(!known)
        {
         ArrayResize(seenSymbols, seen + 1);
         seenSymbols[seen] = symbol;
        }

      //--- Only needed when the account-wide figure does not apply
      if(m_scope == PORTFOLIO_MAGIC_FILTERED)
        {
         ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
         ENUM_ORDER_TYPE asOrder = (posType == POSITION_TYPE_BUY) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
         double legMargin = 0.0;
         if(OrderCalcMargin(asOrder, symbol, volume,
                            PositionGetDouble(POSITION_PRICE_OPEN), legMargin))
            state.usedMargin += legMargin;
        }
     }

   state.symbolCount = ArraySize(seenSymbols);

   if(m_scope == PORTFOLIO_ACCOUNT_WIDE)
      state.usedMargin = AccountInfoDouble(ACCOUNT_MARGIN);

   for(int i = OrdersTotal() - 1; i >= 0; i--)
     {
      ulong ticket = OrderGetTicket(i);
      if(ticket == 0)
         continue;
      if(!IsTracked(OrderGetInteger(ORDER_MAGIC)))
         continue;

      state.pendingCount++;
     }

   return (state.positionCount > 0 || state.pendingCount > 0);
  }

Pending orders are counted separately from positions. An order waiting to trigger is a decision already made, and a limit that ignores it will be breached by orders that were placed before the check ran.


GetCurrencyExposure(): Splitting a Pair Into Two Legs

This is the part that finds correlated exposure.

Step 1: Ask the broker what the symbol is made of.

Do not parse symbol names. Slicing the first three characters works on exactly one broker's naming, then breaks silently on symbol names like EURUSD.raw, EURUSDm, or EURUSD_i. Not with an error. With a wrong answer that looks right. The broker already knows and will say so:

string baseCurrency  = SymbolInfoString(symbol, SYMBOL_CURRENCY_BASE);
string quoteCurrency = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT);

Both are documented under the symbol properties.

Step 2: Turn one position into two currency entries.

A long position in a pair is long the base currency and short the quote currency, in the same volume. A short position is the reverse.

double signedLots = (posType == POSITION_TYPE_BUY) ? volume : -volume;

      //--- Long the pair means long the base and short the quote
      AccumulateCurrency(exposure, baseCurrency,  signedLots,  true);
      AccumulateCurrency(exposure, quoteCurrency, -signedLots, false);

Splitting a pair into its two currency legs to measure exposure is a long-standing approach among discretionary FX traders rather than anything invented here. The article credits it as a common technique.

Step 3: The accumulator.

It finds the currency in the list, creates the entry if it is new, and adds to it. The position count is incremented on the base leg only, so a position is counted once rather than twice.

//+------------------------------------------------------------------+
//| Add signed exposure to a currency, creating its entry if needed  |
//+------------------------------------------------------------------+
void CPortfolioRisk::AccumulateCurrency(SCurrencyExposure &list[], const string currency,
                                        const double signedLots, const bool countPosition)
  {
   if(currency == "")
      return;

   int index = FindCurrency(list, currency);
   if(index < 0)
     {
      index = ArraySize(list);
      ArrayResize(list, index + 1);
      list[index].currency      = currency;
      list[index].netLots       = 0.0;
      list[index].grossLots     = 0.0;
      list[index].positionCount = 0;
     }

   list[index].netLots   += signedLots;
   list[index].grossLots += MathAbs(signedLots);
   if(countPosition)
      list[index].positionCount++;
  }

Step 4: Combine the three steps into one function.

//+------------------------------------------------------------------+
//| Break exposure down by currency.                                 |
//+------------------------------------------------------------------+
int CPortfolioRisk::GetCurrencyExposure(SCurrencyExposure &exposure[])
  {
   ArrayResize(exposure, 0);

   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(!PositionSelectByTicket(ticket))
         continue;
      if(!IsTracked(PositionGetInteger(POSITION_MAGIC)))
         continue;

      string symbol = PositionGetString(POSITION_SYMBOL);
      double volume = PositionGetDouble(POSITION_VOLUME);
      ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);

      string baseCurrency  = SymbolInfoString(symbol, SYMBOL_CURRENCY_BASE);
      string quoteCurrency = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT);

      double signedLots = (posType == POSITION_TYPE_BUY) ? volume : -volume;

      //--- Long the pair means long the base and short the quote
      AccumulateCurrency(exposure, baseCurrency,  signedLots,  true);
      AccumulateCurrency(exposure, quoteCurrency, -signedLots, false);
     }

   return ArraySize(exposure);
  }

Correlation is now addition. No lookback window, no price history, and nothing to tune.

Reading the Currency Table

Take the three positions from the start of this article:

Position Base leg Quote leg
Buy EURUSD 1.00 EUR +1.00 USD -1.00
Buy GBPUSD 1.00 GBP +1.00 USD -1.00
Sell USDJPY 0.5 USD -0.5 JPY +0.5
Net USD
-2.5

These figures illustrate the calculation. They are not results from a test run. Three symbols and two and a half lots of the account pointed the same way. With the currency limit set to two lots, the third trade never opens. Exposure here is measured in lots, and lots are not a notional value. On the base leg the comparison is exact, because one lot of EURUSD and one lot of EURJPY are both a hundred thousand euros. On the quote leg the amounts sit in different currencies, so a net figure there mixes units. Treat quote-side numbers as a concentration signal, not a total.


CanOpenPosition(): The Gate

One function answers the only question an EA needs to ask. It returns a reason string alongside the verdict, so a refusal can be logged in words.

# What it limits Compared against
1 Total positions and pending orders
The whole tracked portfolio
2 Positions on a single symbol
That symbol's own count
3 How many distinct symbols are traded
The cost of symbols in play
4 Margin consumed
Equity
5 Aggregate floating loss
Balance
6 Net exposure to one currency
The currency table

Every check asks what the portfolio would look like with this trade included, not what it looks like now. If you check first and add later, you can exceed the ceiling you intended to enforce. Check three has a wrinkle. The limit is on how many symbols are in play, not on how many trades those symbols may hold, so a second trade on a symbol already being traded does not widen anything and must be allowed through.

//+------------------------------------------------------------------+
//| The gate: can this trade be added to the portfolio?              |
//+------------------------------------------------------------------+
bool CPortfolioRisk::CanOpenPosition(const string symbol, const ENUM_ORDER_TYPE orderType,
                                     const double volume, const double price, string &reason)
  {
   reason = "";

   SPortfolioState state;
   Scan(state);

//--- Total position count
   if(m_maxTotalPositions > 0 && state.positionCount + state.pendingCount >= m_maxTotalPositions)
     {
      reason = StringFormat("portfolio already holds %d position(s)/order(s), limit is %d",
                            state.positionCount + state.pendingCount, m_maxTotalPositions);
      return false;
     }

//--- Concentration in one symbol
   if(m_maxPositionsPerSymbol > 0)
     {
      int symbolCount = 0;
      for(int i = PositionsTotal() - 1; i >= 0; i--)
        {
         ulong ticket = PositionGetTicket(i);
         if(!PositionSelectByTicket(ticket))
            continue;
         if(!IsTracked(PositionGetInteger(POSITION_MAGIC)))
            continue;
         if(PositionGetString(POSITION_SYMBOL) == symbol)
            symbolCount++;
        }

      if(symbolCount >= m_maxPositionsPerSymbol)
        {
         reason = StringFormat("%s already holds %d position(s), limit is %d",
                               symbol, symbolCount, m_maxPositionsPerSymbol);
         return false;
        }
     }

//--- Number of distinct symbols in play
   if(m_maxSymbols > 0 && state.symbolCount >= m_maxSymbols)
     {
      bool alreadyTrading = false;
      for(int i = PositionsTotal() - 1; i >= 0; i--)
        {
         ulong ticket = PositionGetTicket(i);
         if(!PositionSelectByTicket(ticket))
            continue;
         if(!IsTracked(PositionGetInteger(POSITION_MAGIC)))
            continue;
         if(PositionGetString(POSITION_SYMBOL) == symbol)
           {
            alreadyTrading = true;
            break;
           }
        }

      //--- Only a new symbol would widen the portfolio
      if(!alreadyTrading)
        {
         reason = StringFormat("portfolio already spans %d symbol(s), limit is %d",
                               state.symbolCount, m_maxSymbols);
         return false;
        }
     }

//--- Total margin consumption, projected
   if(m_maxMarginPercent > 0 && state.equity > 0)
     {
      double newLegMargin = 0.0;
      if(!OrderCalcMargin(orderType, symbol, volume, price, newLegMargin))
        {
         reason = StringFormat("OrderCalcMargin() failed for %s, error %d", symbol, GetLastError());
         return false;
        }

      double projected = state.usedMargin + newLegMargin;
      double ceiling   = state.equity * (m_maxMarginPercent / 100.0);

      if(projected > ceiling)
        {
         reason = StringFormat("margin would reach %.2f of %.2f allowed (%.1f%% of equity)",
                               projected, ceiling, m_maxMarginPercent);
         return false;
        }
     }

//--- Aggregate floating loss
   if(m_maxLossPercent > 0 && state.balance > 0)
     {
      double maxLoss = state.balance * (m_maxLossPercent / 100.0);
      if(state.floatingPL <= -maxLoss)
        {
         reason = StringFormat("portfolio is already down %.2f, limit is %.2f (%.1f%%)",
                               state.floatingPL, maxLoss, m_maxLossPercent);
         return false;
        }
     }

//--- Currency concentration, projected.
   if(m_maxNetCurrencyLots > 0)
     {
      SCurrencyExposure exposure[];
      GetCurrencyExposure(exposure);

      string baseCurrency  = SymbolInfoString(symbol, SYMBOL_CURRENCY_BASE);
      string quoteCurrency = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT);

      int direction = 0;
      switch(orderType)
        {
         case ORDER_TYPE_BUY:
         case ORDER_TYPE_BUY_LIMIT:
         case ORDER_TYPE_BUY_STOP:
            direction = 1;
            break;
         default:
            direction = -1;
            break;
        }

      double signedLots = volume * direction;

      AccumulateCurrency(exposure, baseCurrency,  signedLots,  false);
      AccumulateCurrency(exposure, quoteCurrency, -signedLots, false);

      int baseIndex  = FindCurrency(exposure, baseCurrency);
      int quoteIndex = FindCurrency(exposure, quoteCurrency);

      if(baseIndex >= 0 && MathAbs(exposure[baseIndex].netLots) > m_maxNetCurrencyLots)
        {
         reason = StringFormat("net %s exposure would reach %+.2f lots, limit is %.2f",
                               baseCurrency, exposure[baseIndex].netLots, m_maxNetCurrencyLots);
         return false;
        }

      if(quoteIndex >= 0 && MathAbs(exposure[quoteIndex].netLots) > m_maxNetCurrencyLots)
        {
         reason = StringFormat("net %s exposure would reach %+.2f lots, limit is %.2f",
                               quoteCurrency, exposure[quoteIndex].netLots, m_maxNetCurrencyLots);
         return false;
        }
     }

   return true;
  }

Check six adds the prospective trade to the exposure table before reading the total. The word "would" in the reason string is the whole design.


PrintSnapShot(): What the EA Reports

The last public function prints the portfolio picture so a user can see what the library sees.

//+------------------------------------------------------------------+
//| Log the current portfolio picture                                |
//+------------------------------------------------------------------+
void CPortfolioRisk::PrintSnapshot(void)
  {
   SPortfolioState state;
   Scan(state);

   PrintFormat("PORTFOLIO | Positions: %d across %d symbol(s) | Pending: %d | Volume: %.2f | Floating: %.2f | Margin used: %.2f | Margin level: %.1f%%",
               state.positionCount, state.symbolCount, state.pendingCount,
               state.totalVolume, state.floatingPL, state.usedMargin, state.marginLevel);

   SCurrencyExposure exposure[];
   int count = GetCurrencyExposure(exposure);

   for(int i = 0; i < count; i++)
      if(MathAbs(exposure[i].netLots) > 0.0)
         PrintFormat("   %s | Net: %+.2f lots | Gross: %.2f lots",
                     exposure[i].currency, exposure[i].netLots, exposure[i].grossLots);
  }

One summary line, then one indented line per currency carrying anything. Currencies that net to zero are skipped, so the log stays readable. The header is complete.


Section 2: Using the Library in the Series EA

Including the File

The library is a separate file, so the EA has to be told where it is. Copy the folder into your MQL5\Included\ directory first.

#include <PortfolioRisk.mqh>

Then one instance was declared with the EA's other globals:

CPortfolioRisk portfolio;


The Inputs

Eleven inputs cover the whole feature. Every numeric limit accepts zero to switch that check off individually.

input group "Portfolio Risk (account-wide)"
input bool                 EnablePortfolioRisk = true;       // Check account-wide limits before trading
input ENUM_PORTFOLIO_SCOPE PortfolioScope = PORTFOLIO_ACCOUNT_WIDE; // What counts as the portfolio
input string               ExtraPortfolioMagics = "";        // Extra magic numbers, comma-separated (filtered scope)
input int                  MaxPortfolioPositions = 6;        // Max positions across the whole portfolio [0 = off]
input int                  MaxPositionsPerSymbol = 2;        // Max positions on any one symbol [0 = off]
input int                  MaxPortfolioSymbols = 4;          // Max distinct symbols traded at once [0 = off]
input double               MaxPortfolioMarginPercent = 40.0; // Max % of equity tied up in margin [0 = off]
input double               MaxPortfolioLossPercent = 6.0;    // Block new trades past this floating loss % [0 = off]
input double               MaxNetCurrencyLots = 2.0;         // Max net lots exposed to one currency [0 = off]
input bool                 LogPortfolioSnapshot = false;     // Log the portfolio picture every bar


Configuring the Class at Startup

The settings are pushed into the class once, in OnInit().

//--- Configure the shared portfolio view
   if(EnablePortfolioRisk)
     {
      portfolio.SetScope(PortfolioScope);
      portfolio.RegisterMagic(MagicNumber);
      RegisterExtraMagics();

      portfolio.SetPositionLimits(MaxPortfolioPositions, MaxPositionsPerSymbol, MaxPortfolioSymbols);
      portfolio.SetMarginLimit(MaxPortfolioMarginPercent);
      portfolio.SetLossLimit(MaxPortfolioLossPercent);
      portfolio.SetCurrencyLimit(MaxNetCurrencyLots);

      if(PortfolioScope == PORTFOLIO_MAGIC_FILTERED && ExtraPortfolioMagics == "")
         Print("Portfolio scope is magic-filtered but no extra magic numbers were listed, so only this EA is counted.");
     }

The EA registers its own magic number first, then any others the user listed. The closing note catches a configuration that compiles and runs but does nothing useful: a filtered scope with an empty list counts only this EA, which is seldom what someone wants.


RegisterExtraMagics(): Reading the Magic List

Under filtered scope, one EA is told about the others through a comma-separated string. That string comes from a text input, so it has to be treated as untrusted.

//+------------------------------------------------------------------+
//| Register the other EAs that share this account's risk budget.    |
//+------------------------------------------------------------------+
void RegisterExtraMagics()
  {
   if(ExtraPortfolioMagics == "")
      return;

   string parts[];
   int count = StringSplit(ExtraPortfolioMagics, StringGetCharacter(",", 0), parts);

   for(int i = 0; i < count; i++)
     {
      string trimmed = parts[i];
      StringTrimLeft(trimmed);
      StringTrimRight(trimmed);

      if(trimmed == "")
         continue;

      long magic = StringToInteger(trimmed);
      if(magic <= 0)
        {
         PrintFormat("Ignoring invalid magic number '%s' in ExtraPortfolioMagics.", trimmed);
         continue;
        }

      portfolio.RegisterMagic(magic);
      PrintFormat("Portfolio now also tracks magic %I64d.", magic);
     }
  }

Someone typing 12345, 55221 produces a leading space on the second entry, which is why both ends are trimmed. A typo becomes a magic number that matches nothing, which is why invalid entries are reported rather than skipped quietly. Anything that reads free text from a user should say what it did with it.


Asking the Validation Gate Before a Trade

The call sits in the validation stage, after the basket rules and before the margin check.

   if(EnablePortfolioRisk)
     {
      string blockReason = "";
      if(!portfolio.CanOpenPosition(_Symbol, MarketOrderTypeOf(plan.orderType),
                                    plan.lotSize, plan.entryPrice, blockReason))
        {
         PrintFormat("Portfolio limit: %s. Trade skipped.", blockReason);
         return false;
        }
     }

The ordering matters. The basket ceiling and the portfolio ceiling are different questions, and both can be right at once. An EA comfortably inside its own basket budget can still be the trade that tips the account over because its basket budget was never told about the other four charts.

The optional snapshot goes in the tick handler, next to the other once-per-bar housekeeping:

   if(EnablePortfolioRisk && LogPortfolioSnapshot)
      portfolio.PrintSnapshot();

It is off by default because a line for every bar becomes noise quickly. It is the first thing to switch on when a trade is refused and the reason is not obvious.

All of this changed with a few lines of the EA. The integration is thin. The EA is not, and it has been growing since the first part.


Pearson Correlation and Why It Is Not Here

Currency decomposition is not the only way to measure correlation. The classical approach is a rolling Pearson correlation: pull closing prices for two symbols over a lookback, compute the coefficient, and treat anything above a threshold as the same bet. It works on any instrument and reflects how symbols are actually behaving rather than what they are nominally made of.

Both properties matter. Gold and EURUSD share no currency leg and can still trade together for months. Indices, crypto, and commodities do not decompose into two currencies at all, so for a portfolio built on those, this library has little to say.

What currency decomposition gives up in exchange is worth having. It is deterministic. It needs no price history, so it works the instant an EA starts. Not only that, but it has no lookback window to tune and therefore none to overtune. Furthermore, it cannot drift, and two EAs on the same account cannot disagree about it. A Pearson coefficient is a moving estimate: it depends on the window and can flip during regime shifts. They are complements rather than rivals. Currency decomposition is the right default because it is always correct about what it measures. Pearson is the proper addition when the portfolio holds instruments that are not currency pairs.


What This Part Does Not Do

Currency decomposition is shaped for foreign exchange. Metals, indices, and crypto will not group meaningfully, and a portfolio built mainly on those gets little from a currency concentration check. Exposure is measured in lots. Those are exactly comparable on the base leg and not comparable on the quote leg, so quote-side figures are a concentration signal rather than a notional total.

There is no coordination between EAs beyond shared reading. Every EA reads the same account state, but they do not talk to each other. Two EAs evaluating on the same tick could both pass the same limit and both open. The probability is low for bar-based EAs, and it is not zero. Proper mitigation needs a shared lock, which is out of scope here.

The margin under the magic-filtered scope is approximate, because MetaTrader publishes no per-position margin figure. None of the measurements survive a terminal restart. Pearson correlation is only discussed and not implemented.


Conclusion

This part moved the unit of risk one step further out. A stop-loss measures one trade. A basket measures one strategy. A portfolio measures the only thing the broker actually cares about.

The technical payoff is the currency table. Once every position is broken into the two currencies it is made of, correlation stops being something you estimate from history and becomes something you add up. Three positions on three symbols resolve into one number pointed at the dollar, and a limit on that number catches concentration that no amount of per-symbol care would have found.

The structural payoff is the header file. A limit that only one participant respects is not a limit, so the measurement had to live outside any single EA. That is why this part produced a library and an EA that consults it, rather than another block of code inside the EA itself.

The source files for the library and the EA are attached below.

Filename Description
FixedMACrossover_Part6.mq5 The series EA now integrates PortfolioRisk.mqh to apply account-wide portfolio checks alongside its existing risk controls.
PortfolioRisk.mqh A reusable portfolio-risk library that measures account exposure and enforces portfolio-wide limits before trades are opened.


Attached files |
PortfolioRisk.mqh (17.8 KB)
Measuring What Matters (Part 4): Reading the Spectrum — What Eigenvalues Tell You About Risk Measuring What Matters (Part 4): Reading the Spectrum — What Eigenvalues Tell You About Risk
We turn eigenvalues from a covariance matrix into a normalized spectral‑entropy score that measures how evenly variance is spread across factors. SpectralEntropyCalculator.mq5 compares two portfolios in one run, using native vector summation, ArraySort()-based ordering, element‑wise division, and the Shannon entropy formula. The report makes dominant factors visible and enables quick, repeatable checks of diversification quality.
Expectancy and Trade Quality Score Dashboard in MQL5 Expectancy and Trade Quality Score Dashboard in MQL5
We present an MQL5 script that converts closed trade history into comparable metrics: expectancy in currency, pips, and R-multiples, plus a sample-aware win rate via the Wilson interval. These inputs form a conservative, dimensionless Trade Quality Score. The tool draws a CCanvas panel, prints an Experts-tab report, and supports an hour-based session filter to analyze a defined trading window alongside full-history results.
Developing a Multi-Currency Expert Advisor (Part 30): From Trading Strategy to Launching a Multi-Currency Expert Advisor Developing a Multi-Currency Expert Advisor (Part 30): From Trading Strategy to Launching a Multi-Currency Expert Advisor
The article outlines the complete process of creating a multi-currency Expert Advisor using the Adwizard library for MetaTrader 5: from setting up the environment for creating optimization projects to obtaining the final multi-currency Expert Advisors, which combine multiple instances of a simple trading strategy. We will walk through setting up the necessary input parameters, conventions for convenient file names, and launching three instances of the final Expert Advisors on different trading accounts with different parameters.
Defining your Edge (Part 4): Applying Isotonic Regression and PNN Price-Forecasting in an Expert Advisor Defining your Edge (Part 4): Applying Isotonic Regression and PNN Price-Forecasting in an Expert Advisor
We consider the methods with which Isotonic Regression calibrates raw RSI, Stochastic and price-action signal scores into probabilities that are sorted, while a separate Probability based Neural Network evaluates similar historical market states. This article uses both approaches in a ready-made MQL5 custom signal class that is compatible with MQL5 Wizard and provides up to 7 selectable entry modes. Reproducible tests compare isotonic-only signals with the combined Isotonic-PNN model to assess whether the network adds useful information beyond the simpler baseline.