preview
Institutional-Grade Multi-Currency Portfolio Engine in MQL5 (Part 1): Architecture of a Multi-Currency EA Framework

Institutional-Grade Multi-Currency Portfolio Engine in MQL5 (Part 1): Architecture of a Multi-Currency EA Framework

MetaTrader 5Trading systems |
138 0
Ushana Kevin Iorkumbul
Ushana Kevin Iorkumbul

Introduction

Most Expert Advisors are designed around a single symbol. The EA opens on a chart, reads price data for that symbol, manages its own positions, and operates entirely within its own execution context. This design works adequately in isolation, but it creates a fundamental problem when deployed across multiple symbols simultaneously: each EA instance is blind to the others.

When five independent EAs run on correlated currency pairs, each sizes positions using its own equity-weighted risk parameters. None of them knows that the other four have already committed capital to highly correlated trades. The result is unintentional risk concentration. A single macro event can move EUR/USD, GBP/USD, and AUD/USD in the same direction. In that case, losses are correlated and effectively form one large drawdown that per-symbol stop-loss logic does not control at the portfolio level.

Solving this requires a shift in design philosophy. Individual EAs must stop being autonomous agents and become coordinated participants in a shared portfolio framework. That coordination requires a dedicated architectural layer.


Master-Slave Paradigm: Roles and Responsibilities

The master-slave paradigm divides responsibility between two categories of EA components: a single Portfolio Controller (the master) and one or more Instrument Agents (the slaves). Each category has a strictly bounded scope.

The Portfolio Controller operates at the portfolio level. It holds no positions directly. Its responsibilities include maintaining aggregate equity exposure, computing portfolio-level risk metrics, broadcasting allocation limits to each Instrument Agent, and enforcing kill conditions when drawdown thresholds are breached. It runs on a dedicated chart (typically a synthetic or low-activity symbol) to receive timer events reliably and to avoid symbol-specific spread widening or execution noise.

Each Instrument Agent manages one symbol. It reads entry and exit signals from its own strategy logic, but before submitting any order, it queries the Portfolio Controller's shared state to confirm that sufficient risk budget remains. If the Portfolio Controller has flagged a risk-off condition, the Instrument Agent does not trade. It does not override this restriction under any circumstance.

This separation produces a clean division between signal generation and capital governance. The strategy logic embedded in each Instrument Agent can be developed, tested, and optimized in isolation, while the Portfolio Controller enforces constraints that no individual agent can circumvent.

Portfolio controller architecture

Figure 1: Block diagram showing the Portfolio Controller at the top tier, connected via shared communication channels to three Agent EA instances, each associated with a separate chart and symbol.

Legend:

  • Gray — Coordination hub: owns risk budget and halt state
  • Green — Trading agent: one instance per managed symbol
  • Solid arrow ↓ — Allocation commands: risk budget, lot limits, halt
  • Dashed arrow ↑ — Position status: open lots, last signal


Inter-EA Communication: The Available Mechanisms

MetaTrader 5 provides several mechanisms for sharing state between EA instances running within the same terminal. Each has distinct performance and reliability characteristics.

Global Variables

MetaTrader 5's terminal-level global variables, accessed via GlobalVariableSet() and GlobalVariableGet(), provide the simplest communication channel. They are stored in shared memory accessible by all EA instances within the same terminal session and survive terminal restarts when written with GlobalVariableSet(). Access is not atomic: a race condition is possible if two EAs write the same variable. In practice, sequential event handling per chart reduces the likelihood, but it does not eliminate it.

Global variables are well suited to broadcasting scalar values: remaining risk budget as a percentage, a portfolio-wide regime flag, a global halt flag, or aggregate open position count. They are not appropriate for transmitting structured data such as arrays, matrices, or position records.

Named Pipes

Named pipes provide a higher-bandwidth, bidirectional communication channel between processes or threads. In the context of MetaTrader 5, they are most useful when the Portfolio Controller needs to transmit structured data to agents — for example, a serialized array of per-symbol position size limits derived from a correlation matrix calculation.

Pipe communication in MQL5 uses Windows API calls via kernel32.dll. The implementation requires careful handling of buffer sizes, blocking versus non-blocking read modes, and connection lifecycle management. The overhead is higher than global variables, making pipes appropriate for periodic structured updates rather than tick-by-tick synchronization.

File-Based Communication

A third option involves writing state to a shared CSV or binary file, which other EA instances read on a timer. This approach is the most portable and easiest to debug, since the shared state is human-readable on disk. Its latency is higher than the other two methods and it introduces file I/O overhead on every timer cycle. For low-frequency rebalancing operations measured in minutes or hours, this overhead is negligible.

The framework built in this series uses global variables as the primary synchronization channel for real-time risk limits and halt flags, with named pipes reserved for the periodic structured updates used in correlation and allocation calculations introduced in later parts.

Comparison table showing the three communication mechanisms:

Dimension Global vars Named pipes File I/O
Latency Low Medium High
Data complexity Scalars only Structured Any format
Implementation overhead Low High Medium
Failure mode Silent stale or zero value Connection drop needs handling Robust, human-readable


Framework Communication Protocol Design

Before writing any code, the communication protocol must be specified precisely. Ambiguous naming conventions or undocumented variable semantics are a primary source of integration bugs in multi-component systems.

The framework uses a prefix-based naming convention for all shared global variables. Each variable name encodes its scope and purpose:

Prefix Scope Example
PC_ Portfolio Controller output PC_RiskBudgetPct
IA_ Instrument Agent report IA_EURUSD_OpenLots
SYS_ System-wide control flags SYS_GlobalHalt

The Portfolio Controller writes to all PC_ variables and reads from all IA_ variables. Each Instrument Agent writes to its own IA_ variables and reads from PC_ variables and SYS_ variables. No Instrument Agent writes to another agent's variables. This asymmetry is enforced by design, not by any runtime lock mechanism, and must be maintained by convention during development.

The full set of variables used in this first implementation is as follows:

Variable Name Data Type Written By Read By Purpose
PC_RiskBudgetPct double Portfolio Controller All Agents Remaining portfolio risk budget as a percentage of equity
PC_MaxLotsPerSymbol double Portfolio Controller All Agents Absolute lot ceiling per instrument at current budget
SYS_GlobalHalt double (0/1) Portfolio Controller All Agents
Emergency flag suspending all new order submissions
IA_[SYMBOL]_OpenLots double
Instrument Agent Portfolio Controller Currently open lot size for the reporting symbol
IA_[SYMBOL]_LastSignal double
Instrument Agent Portfolio Controller Most recent signal direction: 1 (long), -1 (short), 0 (flat)

Using double as the storage type for boolean-like flags such as SYS_GlobalHalt is a deliberate pragmatic choice. MetaTrader 5's GlobalVariableSet() function only accepts double arguments, so casting is unavoidable regardless. Using 0.0 and 1.0 explicitly, rather than implicit casts from bool, eliminates ambiguity when reading values.


Initialization Sequence and Startup Handshake

A multi-component system requires a defined startup order. If an Instrument Agent initializes before the Portfolio Controller has written its first state update, the agent will read stale or uninitialized global variable values — which in MetaTrader 5 default to 0.0 for newly created globals. An uninitialized PC_RiskBudgetPct value of 0.0 would suppress all trading immediately on startup.

The startup handshake solves this with a readiness flag. The Portfolio Controller writes PC_ControllerReady = 1.0 only after it has completed its own initialization and written valid values to all output variables. Each Instrument Agent checks this flag in its OnInit() function and defers entry into the active trading state until the flag is confirmed.

If the flag is absent after a configurable timeout, the Instrument Agent logs a warning and enters degraded mode, using conservative default limits. This mode applies only when the controller is unavailable. This degraded mode ensures the system does not freeze if the Portfolio Controller chart is accidentally closed, but it also means the system operates without portfolio-level coordination — a condition that should trigger an alert to the operator.

Sequence diagram showing the startup handshake

Figure 2: Sequence diagram showing the startup handshake. Portfolio Controller initializes first, writes PC_ControllerReady, then Instrument Agents poll the flag and transition to active state. 


Tick Processing Architecture and Timer Synchronization

In MetaTrader 5, OnTick() fires only for the symbol attached to the chart on which the EA is running. The Portfolio Controller, running on a separate chart, will not receive ticks from the instruments being managed. It must therefore operate on a timer using EventSetTimer().

This introduces a deliberate decoupling. The Portfolio Controller updates portfolio-level state at a fixed interval — typically every 5 to 30 seconds depending on the rebalancing frequency required. Instrument Agents operate on their own symbol's tick stream for signal generation, but they read Portfolio Controller state only when preparing to submit an order. Because orders are prepared far less frequently than ticks arrive, the state is at most one timer cycle out of date.

This design avoids the complexity of synchronizing two high-frequency event streams. The Portfolio Controller does not need to react to every price movement; it needs to react to changes in aggregate position size and equity, which evolve on a slower timescale than individual ticks.

The following table summarizes the event handler responsibilities for each component type:

Event Handler Portfolio Controller Instrument Agent
OnInit() Initialize state, write readiness flag Wait for readiness flag, load symbol metadata
OnTick() Not applicable (no symbol attachment) Execute signal logic, query budget before ordering
OnTimer() Recalculate portfolio metrics, update global variables Periodic heartbeat, log agent state
OnDeinit() Write halt flag, clean up globals Report final position state, deregister


Portfolio Controller: Core Implementation

The Portfolio Controller EA below implements the startup handshake, the timer-based state update cycle, and the global variable broadcast mechanism. It also implements a simple equity-based risk budget calculation that will be extended with volatility targeting and correlation adjustments in later parts of this series.

The budget calculation in this initial version is deliberately simple: the remaining risk budget is expressed as the percentage of current equity that has not yet been committed to open positions. In Part 4, this will be replaced with a volatility-targeted allocation model. The architectural skeleton, however, remains unchanged.

//+------------------------------------------------------------------+
//|                                         PortfolioController.mq5  |
//|          Portfolio-level master EA for multi-currency framework  |
//+------------------------------------------------------------------+

#property version     "1.00"
#property strict

//--- Input parameters
input double inp_MaxPortfolioRiskPct  = 2.0;   // Maximum total portfolio risk %
input double inp_MaxLotsPerSymbol     = 5.0;   // Absolute lot ceiling per symbol
input int    inp_TimerIntervalSeconds = 10;    // Portfolio update interval (seconds)
input int    inp_ReadinessTimeoutSecs = 30;    // Startup readiness timeout (seconds)

//--- Global variable name constants
#define GV_READY            "PC_ControllerReady"
#define GV_RISK_BUDGET      "PC_RiskBudgetPct"
#define GV_MAX_LOTS         "PC_MaxLotsPerSymbol"
#define GV_GLOBAL_HALT      "SYS_GlobalHalt"

//--- Tracked symbols (extend as needed)
string g_ManagedSymbols[] = {"EURUSD","GBPUSD","USDJPY","AUDUSD","USDCAD"};
int    g_SymbolCount      = 5;

//--- Runtime state
double g_CurrentEquity    = 0.0;
double g_TotalOpenLots    = 0.0;
bool   g_HaltActive       = false;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Broadcast halt during initialization to block agents
   GlobalVariableSet(GV_GLOBAL_HALT,  1.0);
   GlobalVariableSet(GV_READY,        0.0);
   GlobalVariableSet(GV_RISK_BUDGET,  0.0);
   GlobalVariableSet(GV_MAX_LOTS,     0.0);

//--- Perform initial portfolio state calculation
   if(!UpdatePortfolioState())
     {
      Print("PortfolioController: Initial state update failed. Aborting init.");
      return(INIT_FAILED);
     }

//--- Broadcast valid initial state before releasing halt
   BroadcastPortfolioState();

//--- Release halt and signal readiness to agents
   GlobalVariableSet(GV_GLOBAL_HALT, 0.0);
   GlobalVariableSet(GV_READY,       1.0);

//--- Start periodic update timer
   EventSetTimer(inp_TimerIntervalSeconds);

   Print("PortfolioController: Initialized. Managing ",
         g_SymbolCount, " symbols.");
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Broadcast halt to all agents before shutdown
   GlobalVariableSet(GV_GLOBAL_HALT, 1.0);
   GlobalVariableSet(GV_READY,       0.0);

   EventKillTimer();

   Print("PortfolioController: Deinitialized. Reason code: ", reason);
  }

//+------------------------------------------------------------------+
//| Timer event: periodic portfolio state recalculation              |
//+------------------------------------------------------------------+
void OnTimer()
  {
   if(!UpdatePortfolioState())
     {
      Print("PortfolioController: State update failed on timer cycle.");
      return;
     }

//--- Check portfolio-level halt conditions
   EvaluateHaltConditions();

//--- Broadcast updated state to all agents
   BroadcastPortfolioState();
  }

//+------------------------------------------------------------------+
//| Recalculate portfolio metrics from current account state         |
//+------------------------------------------------------------------+
bool UpdatePortfolioState()
  {
   g_CurrentEquity = AccountInfoDouble(ACCOUNT_EQUITY);

   if(g_CurrentEquity <= 0.0)
     {
      Print("PortfolioController: Invalid equity reading: ", g_CurrentEquity);
      return(false);
     }

//--- Aggregate open lots across all managed symbols
   g_TotalOpenLots = 0.0;

   for(int i = 0; i < g_SymbolCount; i++)
     {
      string sym = g_ManagedSymbols[i];
      string varName = "IA_" + sym + "_OpenLots";

      if(GlobalVariableCheck(varName))
         g_TotalOpenLots += GlobalVariableGet(varName);
     }

   return(true);
  }

//+------------------------------------------------------------------+
//| Evaluate conditions that should trigger a global halt            |
//+------------------------------------------------------------------+
void EvaluateHaltConditions()
  {
   double balance  = AccountInfoDouble(ACCOUNT_BALANCE);
   double equity   = g_CurrentEquity;

//--- Halt if floating drawdown exceeds 3x the configured risk budget
   double floatingDrawdownPct = 0.0;

   if(balance > 0.0)
      floatingDrawdownPct = ((balance - equity) / balance) * 100.0;

   bool haltRequired = (floatingDrawdownPct >= inp_MaxPortfolioRiskPct * 3.0);

   if(haltRequired && !g_HaltActive)
     {
      Print("PortfolioController: Halt condition triggered. Drawdown: ",
            DoubleToString(floatingDrawdownPct, 2), "%");
      g_HaltActive = true;
     }
   else
      if(!haltRequired && g_HaltActive)
        {
         Print("PortfolioController: Halt condition cleared.");
         g_HaltActive = false;
        }
  }

//+------------------------------------------------------------------+
//| Broadcast current portfolio state to shared global variables     |
//+------------------------------------------------------------------+
void BroadcastPortfolioState()
  {
//--- Calculate remaining risk budget percentage
   double committedRiskPct = 0.0;

   if(g_CurrentEquity > 0.0)
      committedRiskPct = (g_TotalOpenLots / inp_MaxLotsPerSymbol)
                         * inp_MaxPortfolioRiskPct;

   double remainingBudgetPct = MathMax(0.0,
                                       inp_MaxPortfolioRiskPct - committedRiskPct);

//--- Write to shared globals
   GlobalVariableSet(GV_RISK_BUDGET, remainingBudgetPct);
   GlobalVariableSet(GV_MAX_LOTS,    inp_MaxLotsPerSymbol);
   GlobalVariableSet(GV_GLOBAL_HALT, g_HaltActive ? 1.0 : 0.0);
  }
//+------------------------------------------------------------------+


Instrument Agent: Core Implementation

The Instrument Agent below implements the readiness check, the degraded-mode fallback, position state reporting, and a placeholder signal evaluation stub. The signal logic itself — the strategy — is intentionally left minimal. In a production deployment, the EvaluateSignal() function would contain the complete entry and exit logic for the instrument's strategy. Separating signal logic from framework logic is an explicit design goal; the framework layer should be replaceable without touching signal code, and vice versa.

The lot sizing in this version queries PC_MaxLotsPerSymbol from the Portfolio Controller and applies a simple scaling factor based on remaining budget. The volatility-normalized sizing introduced in Part 4 will replace this calculation while leaving the query mechanism unchanged.

//+------------------------------------------------------------------+
//|                                            InstrumentAgent.mq5   |
//|         Instrument-level slave EA for multi-currency framework   |
//+------------------------------------------------------------------+

#property version     "1.00"
#property strict

#include <Trade\Trade.mqh>

//--- Input parameters
input string inp_ManagedSymbol         = "EURUSD";  // Symbol this agent manages
input double inp_BaseRiskPct           = 1.0;       // Base risk % per trade
input int    inp_ReadinessTimeoutSecs  = 60;        // Max wait for PC readiness (s)
input int    inp_MagicNumber           = 100001;    // Order magic number
input int    inp_TimerIntervalSeconds  = 15;        // Agent heartbeat interval (s)

//--- Global variable name constants
#define GV_READY        "PC_ControllerReady"
#define GV_RISK_BUDGET  "PC_RiskBudgetPct"
#define GV_MAX_LOTS     "PC_MaxLotsPerSymbol"
#define GV_GLOBAL_HALT  "SYS_GlobalHalt"

//--- Runtime state
CTrade  g_Trade;
bool    g_PCReady              = false;
bool    g_DegradedMode         = false;
double  g_LastSignal           = 0.0;
ulong   g_LastOrderTicket      = 0;
datetime g_InitTime            = 0;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   g_InitTime = TimeCurrent();

//--- Configure trade object
   g_Trade.SetExpertMagicNumber(inp_MagicNumber);
   g_Trade.SetDeviationInPoints(20);
   g_Trade.SetTypeFilling(ORDER_FILLING_FOK);

//--- Register agent state variables
   string lotsVar   = "IA_" + inp_ManagedSymbol + "_OpenLots";
   string signalVar = "IA_" + inp_ManagedSymbol + "_LastSignal";
   GlobalVariableSet(lotsVar,   0.0);
   GlobalVariableSet(signalVar, 0.0);

//--- Start heartbeat timer
   EventSetTimer(inp_TimerIntervalSeconds);

   Print("InstrumentAgent [", inp_ManagedSymbol, "]: Initialized. ",
         "Waiting for Portfolio Controller readiness.");

   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Report zero open lots on shutdown
   string lotsVar = "IA_" + inp_ManagedSymbol + "_OpenLots";
   GlobalVariableSet(lotsVar, 0.0);

   EventKillTimer();

   Print("InstrumentAgent [", inp_ManagedSymbol, "]: Deinitialized. Reason: ",
         reason);
  }

//+------------------------------------------------------------------+
//| Tick event: signal evaluation and order submission               |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check PC readiness on each tick until confirmed
   if(!g_PCReady)
     {
      CheckControllerReadiness();
      if(!g_PCReady)
         return;
     }

//--- Do not trade if global halt is active
   if(IsGlobalHaltActive())
      return;

//--- Evaluate signal and act
   double signal = EvaluateSignal();
   if(signal == 0.0)
      return;

//--- Compute lot size from portfolio budget
   double lots = ComputeLotSize();
   if(lots <= 0.0)
      return;

//--- Submit order based on signal direction
   if(signal > 0.0)
     {
      if(g_Trade.Buy(lots, inp_ManagedSymbol, 0, 0, 0, "IA_Entry"))
         Print("InstrumentAgent [", inp_ManagedSymbol, "]: BUY submitted. Lots: ",
               DoubleToString(lots, 2));
     }
   else
      if(signal < 0.0)
        {
         if(g_Trade.Sell(lots, inp_ManagedSymbol, 0, 0, 0, "IA_Entry"))
            Print("InstrumentAgent [", inp_ManagedSymbol, "]: SELL submitted. Lots: ",
                  DoubleToString(lots, 2));
        }

//--- Update signal report to Portfolio Controller
   string signalVar = "IA_" + inp_ManagedSymbol + "_LastSignal";
   GlobalVariableSet(signalVar, signal);
  }

//+------------------------------------------------------------------+
//| Timer event: heartbeat and state reporting                       |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- Report current open lots to Portfolio Controller
   ReportOpenLots();

//--- Re-check readiness if not yet confirmed
   if(!g_PCReady)
      CheckControllerReadiness();
  }

//+------------------------------------------------------------------+
//| Check whether the Portfolio Controller has signalled readiness   |
//+------------------------------------------------------------------+
void CheckControllerReadiness()
  {
   bool readyFlagExists = GlobalVariableCheck(GV_READY);
   bool readyFlagSet    = readyFlagExists &&
                          (GlobalVariableGet(GV_READY) == 1.0);

   if(readyFlagSet)
     {
      g_PCReady      = true;
      g_DegradedMode = false;
      Print("InstrumentAgent [", inp_ManagedSymbol,
            "]: Portfolio Controller confirmed ready.");
      return;
     }

//--- Check if timeout has elapsed
   int elapsedSeconds = (int)(TimeCurrent() - g_InitTime);

   if(elapsedSeconds >= inp_ReadinessTimeoutSecs)
     {
      g_PCReady      = true;   // Proceed, but in degraded mode
      g_DegradedMode = true;
      Print("InstrumentAgent [", inp_ManagedSymbol,
            "]: PC readiness timeout. Entering degraded mode.");
     }
  }

//+------------------------------------------------------------------+
//| Return true if the Portfolio Controller has set a global halt    |
//+------------------------------------------------------------------+
bool IsGlobalHaltActive()
  {
   if(!GlobalVariableCheck(GV_GLOBAL_HALT))
      return(false);
   return(GlobalVariableGet(GV_GLOBAL_HALT) == 1.0);
  }

//+------------------------------------------------------------------+
//| Placeholder: strategy signal evaluation                          |
//| Returns 1.0 (long), -1.0 (short), or 0.0 (no signal)             |
//+------------------------------------------------------------------+
double EvaluateSignal()
  {
//--- Stub: replace with symbol-specific strategy logic
   return(0.0);
  }

//+------------------------------------------------------------------+
//| Compute lot size from portfolio budget and account equity        |
//+------------------------------------------------------------------+
double ComputeLotSize()
  {
   double remainingBudget = 0.0;
   double maxLots         = 0.0;

   if(!g_DegradedMode)
     {
      if(GlobalVariableCheck(GV_RISK_BUDGET))
         remainingBudget = GlobalVariableGet(GV_RISK_BUDGET);

      if(GlobalVariableCheck(GV_MAX_LOTS))
         maxLots = GlobalVariableGet(GV_MAX_LOTS);
     }
   else
     {
      //--- Degraded mode: use conservative defaults
      remainingBudget = inp_BaseRiskPct * 0.5;
      maxLots         = 1.0;
     }

   if(remainingBudget <= 0.0 || maxLots <= 0.0)
      return(0.0);

//--- Scale lots proportionally to remaining budget fraction
   double budgetFraction = MathMin(1.0,
                                   remainingBudget / inp_BaseRiskPct);
   double rawLots        = maxLots * budgetFraction;

//--- Normalize to symbol's lot step
   double lotStep = SymbolInfoDouble(inp_ManagedSymbol, SYMBOL_VOLUME_STEP);
   double minLot  = SymbolInfoDouble(inp_ManagedSymbol, SYMBOL_VOLUME_MIN);

   if(lotStep <= 0.0)
      return(0.0);

   double normalizedLots = MathFloor(rawLots / lotStep) * lotStep;

   return(normalizedLots >= minLot ? normalizedLots : 0.0);
  }

//+------------------------------------------------------------------+
//| Report current open lot total for this symbol to the PC         |
//+------------------------------------------------------------------+
void ReportOpenLots()
  {
   double totalLots = 0.0;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0)
         continue;

      if(PositionGetString(POSITION_SYMBOL) == inp_ManagedSymbol &&
         PositionGetInteger(POSITION_MAGIC) == inp_MagicNumber)
        {
         totalLots += PositionGetDouble(POSITION_VOLUME);
        }
     }

   string lotsVar = "IA_" + inp_ManagedSymbol + "_OpenLots";
   GlobalVariableSet(lotsVar, totalLots);
  }
//+------------------------------------------------------------------+


Architectural Trade-offs and Known Constraints

This architecture resolves the risk concentration problem, but it introduces constraints that must be understood before deployment.

Single terminal dependency: Global variables in MetaTrader 5 are terminal-scoped. If the Portfolio Controller and all Instrument Agents run within the same terminal instance, the communication channel is robust. If agents are distributed across multiple terminal instances — for example, because different brokers are used for different symbols — global variables are not shared across them. Named pipes or a file-based intermediate can bridge this gap, but the implementation complexity increases substantially.

No atomic write guarantee: The Portfolio Controller writes multiple global variables in sequence during BroadcastPortfolioState(). An Instrument Agent reading those variables mid-write will see a partially updated state. In this first version, the window of inconsistency is negligible for the data types involved (scalar doubles representing budget fractions), but any future extension that writes correlated values simultaneously — such as per-symbol allocation vectors — should implement a write-lock pattern using a dedicated PC_StateWriting flag variable.

Degraded mode is a safety fallback, not a trading mode: When an Instrument Agent operates in degraded mode, it uses conservative defaults rather than portfolio-informed allocations. The intent is to preserve system availability, not to maintain optimal performance. Any production deployment should treat degraded mode as an alert condition requiring operator intervention.

Timer granularity and burst latency: The EventSetTimer() function in MQL5 has a minimum resolution of approximately one second. In fast market conditions, a 10-second update cycle in the Portfolio Controller means that Instrument Agents may receive an authorization to trade based on state that is up to 10 seconds old. For the position sizing and risk budget calculations used here, this lag is acceptable. For tick-level execution strategies, it is not, and a more responsive synchronization mechanism would be necessary.


Conclusion

This part establishes the foundational communication infrastructure for a multi-currency portfolio engine. The master-slave paradigm separates portfolio governance from instrument-level signal generation, and the global variable protocol provides a lightweight, deterministic synchronization channel between components. The startup handshake prevents agents from trading on uninitialized state, while the degraded mode fallback ensures the system remains operational when the Portfolio Controller is unavailable.

The Portfolio Controller and Instrument Agent implementations produced here are intentionally sparse. The budget calculation is equity-based but not volatility-adjusted. The signal evaluation function is a stub. The lot sizing uses a simple proportional scaling rather than a risk-parity model. Each of these components will be extended in subsequent parts. The architecture, however — the event handling structure, the global variable naming convention, the startup sequence, and the read-write ownership rules — remains stable across the entire series.

Part 2 extends the Instrument Agent with dynamic symbol loading and contract specification normalization, building the metadata layer that the correlation and allocation engines in later parts depend on.


Programs used in the article:

# Name Type Description
1 PortfolioController.mq5 Expert Advisor (Master) Portfolio-level controller EA. Manages aggregate risk budget, evaluates halt conditions, and broadcasts shared state via global variables on a timer cycle.
2 InstrumentAgent.mq5 Expert Advisor (Slave) Instrument-level agent EA. Reads portfolio state from the controller, evaluates entry signals, sizes positions against available budget, and reports open lot state back to the controller.
3 Multi_Currency_Portfolio_Engine_Part_1.zip Zip Archive Zip archive containing all the attached files and their paths relative to the terminal's root folder.
Building a Hidden Risk of Ruin Auditor in MQL5 Building a Hidden Risk of Ruin Auditor in MQL5
Aggregate metrics alone do not reveal how a trade sequence manages risk. This MQL5 tool analyzes closed positions to flag four structural patterns: post-loss volume escalation, overlapping same-direction entries, asymmetric payoffs, and a classical risk-of-ruin figure. The results are merged into a configurable A-F grade with concise recommendations to guide further review.
Beyond the Mean and Standard Deviation: A Robust Statistics Library for MQL5 Indicators Beyond the Mean and Standard Deviation: A Robust Statistics Library for MQL5 Indicators
Price outliers distort indicators based on the mean and standard deviation. This article delivers a robust MQL5 library (RobustStats.mqh) implementing the median, 1.4826-scaled MAD, and Theil–Sen slope, plus three drop‑in indicators that replace Bollinger Bands, the linear regression channel, and the z‑score oscillator. A comparison overlay and a breakdown‑point measurement on EURUSD show how the robust instruments hold their shape when a single spike moves the classical ones.
Developing Smart Chart Objects in MQL5 (Part 1): Building a Stateful Trendline Management Framework Developing Smart Chart Objects in MQL5 (Part 1): Building a Stateful Trendline Management Framework
This article details a practical framework for converting MetaTrader 5 trendlines from static drawings into managed runtime entities. It covers object discovery, event-driven synchronization of user edits, and confirmation logic based on ATR multipliers and closed candles. A central manager coordinates multiple lines and updates their visual state. Readers can implement consistent, extensible rules for detecting proximity, validating bounces, and confirming breakouts.
Python + LLM API + MetaTrader 5: Real-World Experience Building an Autonomous Trading Bot Python + LLM API + MetaTrader 5: Real-World Experience Building an Autonomous Trading Bot
The article describes the development of an MVP prototype for an autonomous trading bot for MetaTrader 5 that uses large language models (LLMs) via the OpenRouter API to analyze the market and make trading decisions. A Python script retrieves historical OHLCV data, sends it to an LLM for technical analysis based on support/resistance levels and Price Action patterns, and then automatically places orders with specified stop loss and take profit levels.