Building Your Personal Expert Advisor (Part 6): Risk Management V — Portfolio and Correlated Risk
Contents
- Introduction
- Problem: Every EA Believes It Is Alone
- Why Portfolio Risk Belongs in a Header File
- Scope of This Article
- Section 1: Building PortfolioRisk.mqh
- Section 2: Using the Library in the Series EA
- Pearson Correlation and Why It Is Not Here
- What This Part Does Not Do
- 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.

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.

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);
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.
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. |
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Measuring What Matters (Part 4): Reading the Spectrum — What Eigenvalues Tell You About Risk
Expectancy and Trade Quality Score Dashboard in MQL5
Developing a Multi-Currency Expert Advisor (Part 30): From Trading Strategy to Launching a Multi-Currency Expert Advisor
Defining your Edge (Part 4): Applying Isotonic Regression and PNN Price-Forecasting in an Expert Advisor
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use