Institutional-Grade Multi-Currency Portfolio Engine in MQL5 (Part 1): Architecture of a Multi-Currency EA Framework
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.

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.

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. |
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.
Building a Hidden Risk of Ruin Auditor in MQL5
Beyond the Mean and Standard Deviation: A Robust Statistics Library for MQL5 Indicators
Developing Smart Chart Objects in MQL5 (Part 1): Building a Stateful Trendline Management Framework
Python + LLM API + MetaTrader 5: Real-World Experience Building an Autonomous Trading Bot
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use