The MQL5 Standard Library Explorer (Part 17): Building a Reusable Portfolio Manager with CArrayObj
You already separated classification, decision control and execution inside the EA, but a practical bottleneck remains: the program still treats one symbol at a time. Adding a second instrument quickly multiplies state and checks — separate bar times, magic numbers, failure flags, position checks and event handling — and mixes per-symbol state with MetaTrader 5 event logic. That leads to duplicated code, fragile lifecycle handling (leaks or double deletes), and poor scalability.
This article presents a different approach: encapsulate every symbol-specific state and behavior in an independent CStrategyNode, and let a lightweight CPortfolioManager own and route work to those nodes. CArrayObj (FreeMode(true)) implements a clear ownership contract: allocate an unowned node, Initialize() it, then Add() it (the commit point) so the collection becomes responsible for deletion. A short timer centralizes scheduling while each node keeps its own completed-bar gate and failure state. By the end you will have a practical, testable scaffold for multi-symbol EAs that separates concerns, ensures safe memory ownership, and isolates node failures without duplicating event logic.
Contents
- Why Object Collections
- Portfolio Architecture
- Understanding CArrayObj Ownership
- Building the Strategy Node
- Independent Completed-Bar Processing
- Position and Risk Boundaries
- Building the Portfolio Manager
- Transferring Object Ownership
- Timer-Driven Event Routing
- Operating the Expert Advisor
- Testing Results and Remaining Checks
- Limitations
- Conclusion
- Key Lessons
- Attachments
Why Object Collections
A multi-symbol EA can be written with separate global variables for EURUSD, GBPUSD, USDJPY, and every other instrument. That approach initially appears simple, but each additional symbol introduces another bar time, signal, magic number, failure state, and position check. The program grows horizontally through duplication rather than structurally through reuse.
The alternative is to describe what one managed symbol needs and instantiate that description several times. In object-oriented terms, the description is a class and each managed symbol is an object. A portfolio manager then needs a container capable of storing pointers to those objects, iterating through them, and releasing them safely.
The Standard Library's CArrayObj provides a dynamic array of pointers to CObject and derived classes. It supports insertion, access, deletion, sorting, searching, file operations, and configurable memory management. Part 17 concentrates on the operations required for a safe owning collection: FreeMode(), Add(), At(), Delete(), Total(), and Clear().
Before choosing classes, we can translate the original pain into four requirements. These requirements guide the rest of the implementation and provide the criteria used during testing.
| Observed problem | Required property | Implementation boundary | Evidence to inspect |
|---|---|---|---|
| Symbol variables and checks are duplicated | Encapsulated symbol-timeframe state | One CStrategyNode per managed identity | Independent node summaries and configuration |
| Host-chart ticks do not represent every symbol | Shared event routing with independent bar gates | OnTimer() → ProcessAll() → node IsNewBar() | Timer passes advance while each node evaluates only a new completed bar |
| Pointer cleanup can leak or delete twice | One explicit ownership transfer | Local allocation → Initialize() → successful Add() | Failure before Add() deletes locally; Delete()/Clear() handle accepted nodes |
| One unavailable symbol can disrupt portfolio work | Node-level diagnostics and continued iteration | ProcessAll() records a failed attempt and advances | Affected-node status plus continued calls to later nodes |
The sections that follow implement these boundaries in the same order: node state, collection ownership, completed-bar processing, failure isolation, and host-event routing.
Portfolio Architecture
The implementation is divided into three source files. StrategyNode.mqh contains the state and behavior of one symbol-timeframe identity. PortfolioManager.mqh owns the collection and routes work to every node. ReusablePortfolioManagerEA.mq5 connects the manager to MetaTrader 5 event handlers.

Fig. 1. Reusable portfolio manager architecture and ownership boundary
| Component | Responsibility | Owned state |
|---|---|---|
| Host EA | Receives initialization, timer, trade-transaction, and deinitialization events | One CPortfolioManager |
| CPortfolioManager | Creates, locates, processes, removes, and summarizes nodes | One CArrayObj collection |
| CArrayObj | Stores and releases pointers to strategy nodes | Every successfully added CStrategyNode |
| CStrategyNode | Processes one symbol and timeframe independently | Bar time, signal, magic, trade objects, risk settings, failures, and status |
The ownership boundary is more important than the number of classes. The manager creates a node, but the collection owns it only after Add() succeeds. This distinction determines which component must delete the pointer on every failure path.
Understanding CArrayObj Ownership
CArrayObj stores CObject pointers. CStrategyNode therefore inherits from CObject, making each node compatible with the collection while allowing it to retain specialized methods and fields.
//+------------------------------------------------------------------+ //| Directional state of a node: -1 short, 0 neutral, +1 long | //+------------------------------------------------------------------+ enum ENUM_NODE_SIGNAL { NODE_SIGNAL_SHORT=-1, NODE_SIGNAL_NEUTRAL=0, NODE_SIGNAL_LONG=1 }; //+------------------------------------------------------------------+ //| One self-contained strategy unit per portfolio symbol | //+------------------------------------------------------------------+ class CStrategyNode : public CObject { private: //--- Identity and configuration string m_symbol; // Traded symbol ENUM_TIMEFRAMES m_timeframe; // Timeframe for bars and signals ulong m_magic; // Magic identifying this node's trades bool m_allow_trading; // Master switch: monitor or trade double m_signal_threshold;// |Bar return| that fires a signal double m_risk_percent; // Equity % risked per trade int m_stop_points; // Base stop loss distance in points double m_reward_risk; // Take profit as SL multiple int m_max_spread; // Max spread in points for entries //--- Runtime state datetime m_last_bar; // Open time of the last processed bar ENUM_NODE_SIGNAL m_signal; // Current evaluated signal bool m_initialized; // True after Initialize() succeeds bool m_enabled; // False after Shutdown() //--- Bookkeeping and helpers int m_failures; // Cumulative failure counter string m_status; // Human-readable node status CTrade m_trade; // Dedicated order object (own magic) CPositionInfo m_position; // Cached view of this node's position bool IsNewBar(void); bool EvaluateSignal(void); bool HasForeignPosition(void); bool TradeResultSucceeded(void); double NormalizeVolume(const double requested) const; double CalculateVolume(const double stop_distance) const; bool CloseOwnedPosition(const string reason); bool OpenPosition(void); void ApplySignal(void); void Fail(const string message); public: CStrategyNode(void); ~CStrategyNode(void); bool Initialize(const string symbol, const ENUM_TIMEFRAMES timeframe, const ulong magic, const bool allow_trading, const double signal_threshold, const double risk_percent, const int stop_points, const double reward_risk, const int max_spread); bool Process(void); void RefreshPosition(void); void Shutdown(void); string Symbol(void) const { return(m_symbol); } ENUM_TIMEFRAMES Timeframe(void) const { return(m_timeframe); } ulong Magic(void) const { return(m_magic); } bool Enabled(void) const { return(m_enabled); } int Failures(void) const { return(m_failures); } string Status(void) const { return(m_status); } string SignalName(void) const; string Summary(void); };
The declaration divides the node into three deliberate layers. Identity and configuration remain stable after initialization; runtime fields change as bars and positions evolve; helper methods keep trading mechanics behind the public interface. The numeric signal values also make direction explicit: negative means short, zero means neutral, and positive means long. The manager therefore coordinates nodes through Process(), Shutdown(), and Summary() without manipulating their internal trading state.
CArrayObj enables memory management by default, but the manager states the policy explicitly:
//+------------------------------------------------------------------+ //| Constructor: enables FreeMode ownership of added nodes | //+------------------------------------------------------------------+ CPortfolioManager::CPortfolioManager(void) { m_nodes.FreeMode(true); m_cycles=0; m_failed_cycles=0; }
FreeMode(true) establishes the destruction policy before the first pointer can enter the collection. It does not allocate or initialize a node; it tells CArrayObj what to do when an owned element is deleted or the array is cleared. Initializing the cycle counters in the same constructor gives the manager one consistent starting state for both ownership and diagnostics.
With FreeMode(true), Delete() releases the removed element and Clear() releases all stored elements. The program must not call delete on a pointer after it has entered the collection. If a newly allocated node fails Initialize() or Add(), it remains unowned. The allocating code must delete it.

Fig. 2. Transformation from duplicated globals to an owned node collection
Building the Strategy Node
Initialize() receives everything needed to make one node independent: symbol, timeframe, magic number, execution permission, signal threshold, risk allocation, stop distance, reward-to-risk ratio, and maximum spread. It selects the symbol in Market Watch and configures a dedicated CTrade object.
//+------------------------------------------------------------------+ //| Validates inputs, selects the symbol and configures the node | //+------------------------------------------------------------------+ bool CStrategyNode::Initialize(const string symbol, const ENUM_TIMEFRAMES timeframe, const ulong magic, const bool allow_trading, const double signal_threshold, const double risk_percent, const int stop_points, const double reward_risk, const int max_spread) { if(symbol=="" || magic==0 || signal_threshold<=0.0 || risk_percent<=0.0 || stop_points<1 || reward_risk<=0.0 || max_spread<0) return(false); if(!SymbolSelect(symbol,true)) return(false); m_symbol=symbol; m_timeframe=timeframe; m_magic=magic; m_allow_trading=allow_trading; m_signal_threshold=signal_threshold; m_risk_percent=risk_percent; m_stop_points=stop_points; m_reward_risk=reward_risk; m_max_spread=max_spread; m_trade.SetExpertMagicNumber(m_magic); m_trade.SetDeviationInPoints(20); if(!m_trade.SetTypeFillingBySymbol(m_symbol)) return(false); m_last_bar=iTime(m_symbol,m_timeframe,0); m_initialized=true; m_enabled=true; m_status="READY"; return(true); }
Initialize() is transactional with respect to node ownership: the manager either receives an initialized node or deletes the still-unowned allocation. It is not free of terminal side effects. A successful SymbolSelect() call can leave the symbol selected in Market Watch even if a later configuration step fails.
The routine validates scalar settings, stores the node identity, assigns the requested magic number to its CTrade object, and selects a filling policy supported by the symbol. Current MQL5 documentation defines SetTypeFillingBySymbol() as a Boolean operation, so the conditional failure check is intentional. The call to iTime() supplies the initial bar timestamp, but zero remains possible while history is unavailable. A true return therefore confirms node configuration, not complete series readiness; later CopyRates() checks remain the authoritative data gate.
AddNode() may transfer the pointer to CArrayObj only after Initialize() returns true. The host constructs magic numbers from InpBaseMagic and the input-list index, which makes that host-generated sequence reproducible. AddNode() itself rejects duplicate symbol-timeframe identities but does not independently reject a magic number already assigned to another node.
The trading rule is deliberately simple. It measures the return between the two most recently completed bars. A return above the positive threshold produces a long signal, a return below the negative threshold produces a short signal, and movement inside the band remains neutral. This rule makes independent node behavior observable without turning the article into a strategy-optimization study.
Independent Completed-Bar Processing
The host timer may fire every second, but a strategy node should not reevaluate the same completed bar repeatedly. Each node therefore retains its own m_last_bar value. This is necessary because symbols may receive data at different times, and one synchronized symbol must not force another node to pretend that its history is ready.
//+------------------------------------------------------------------+ //| One decision cycle: new bar -> signal -> apply action | //+------------------------------------------------------------------+ bool CStrategyNode::Process(void) { if(!m_initialized || !m_enabled) return(false); if(!IsNewBar()) return(true); if(!EvaluateSignal()) { Fail("HISTORY NOT READY"); return(false); } ApplySignal(); return(true); }
Process() distinguishes normal inactivity from an operational failure. Returning true when no new bar exists means the node was checked successfully and requires no work. Returning false after EvaluateSignal() fails means a required history read was unavailable, so Fail() records a diagnostic. This distinction lets the manager count failed node-processing attempts without treating every timer event between H1 bars as an error.
EvaluateSignal() requests two completed bars with CopyRates() beginning at shift 1. It verifies the copy count and the denominator before calculating the return. If history is temporarily unavailable, only that node reports a failure. ProcessAll() continues to the next element.
Position and Risk Boundaries
The host EA calculates a reproducible magic-number sequence from InpBaseMagic and each input-list position. This is deterministic for that host configuration, but the manager does not enforce global magic-number uniqueness when AddNode() is called from other code. The node selects its own position with CPositionInfo::SelectByMagic(). It also scans positions on its symbol and blocks trading when another magic number is found. This conservative rule prevents the portfolio example from modifying trades owned by another EA or a manual process.
InpAllowTrading defaults to false. When it remains false, the complete portfolio continues to load history, detect completed bars, calculate signals, count failures, and update the chart summary without submitting orders.
When trading is enabled, each node calculates volume from its fraction of current equity:
V = (E × R / 100) / ((D / Tₛ) × Tᵥ)
where V is volume in lots, E is account equity in the deposit currency, R is the node risk percentage, D is the stop distance in price units, Tₛ is tick size in price units, and Tᵥ is the reported loss tick value per lot. The formula is an educational estimate. The node rejects zero or invalid tick properties and rounds volume downward to the symbol step, but it does not replace OrderCalcMargin(), portfolio exposure checks, or instrument-specific validation for every profit currency and contract type.
The node also checks trading permissions (terminal, program, account, and symbol), spread, minimum stops level, filling mode, symbol digits, and the trade-server return code. A signal alone cannot bypass these boundaries.
Building the Portfolio Manager
CPortfolioManager provides one interface for the host EA. It contains no strategy rule. Its responsibility is lifecycle coordination.
//+------------------------------------------------------------------+ //| Coordinates the lifecycle and cycles of all strategy nodes | //+------------------------------------------------------------------+ class CPortfolioManager { private: CArrayObj m_nodes; // Owned node container (FreeMode) int m_cycles; // Timer cycles processed int m_failed_cycles; // Node failures seen across cycles public: CPortfolioManager(void); ~CPortfolioManager(void); bool AddNode(const string symbol, const ENUM_TIMEFRAMES timeframe, const ulong magic, const bool allow_trading, const double signal_threshold, const double risk_percent, const int stop_points, const double reward_risk, const int max_spread); CStrategyNode *FindNode(const string symbol, const ENUM_TIMEFRAMES timeframe); void ProcessAll(void); void RefreshAllPositions(void); bool RemoveNode(const string symbol, const ENUM_TIMEFRAMES timeframe); int TotalNodes(void) const { return(m_nodes.Total()); } string Summary(void); void Shutdown(void); };
The manager stores CObject-compatible pointers, but its public methods expose portfolio concepts rather than array mechanics. AddNode() and RemoveNode() control membership, FindNode() resolves symbol-timeframe identity, and ProcessAll() routes work. This boundary prevents the host EA from depending on collection indexes, which may change after an element is deleted, and keeps ownership rules centralized.
FindNode() performs a linear search because the initial portfolio is small and the article benefits from visible logic. At() returns a CObject pointer, which is cast back to CStrategyNode because this manager accepts only that object type. A future heterogeneous plugin framework should introduce explicit type validation before casting.
//+------------------------------------------------------------------+ //| Runs one cycle per node; failures are counted, never propagated | //+------------------------------------------------------------------+ void CPortfolioManager::ProcessAll(void) { m_cycles++; for(int i=0;i<m_nodes.Total();i++) { CStrategyNode *node=(CStrategyNode*)m_nodes.At(i); if(node==NULL) { m_failed_cycles++; continue; } if(!node.Process()) m_failed_cycles++; } }
Despite its historical name, m_failed_cycles counts failed node-processing attempts. It does not count failed portfolio cycles, distinct failed symbols, or recovery events. A temporarily unavailable history series can therefore increment the counter several times and later succeed. The loop always advances to the next index, so one failed call does not prevent the manager from invoking later nodes. The counter is diagnostic evidence that failures occurred; it does not by itself prove their cause or successful recovery.
A failed node increments a portfolio diagnostic but does not terminate the loop. This is failure isolation, not failure suppression: each node records the reason in its own status and journal message, while the manager records the number of unsuccessful node cycles.
Transferring Object Ownership
AddNode() is the most important memory-management routine in the article. It rejects duplicate symbol-timeframe identities, allocates a node, initializes it, and then transfers the pointer to CArrayObj.
//+------------------------------------------------------------------+ //| Creates, initializes, and stores one node; rejects duplicates | //+------------------------------------------------------------------+ bool CPortfolioManager::AddNode(const string symbol, const ENUM_TIMEFRAMES timeframe, const ulong magic, const bool allow_trading, const double signal_threshold, const double risk_percent, const int stop_points, const double reward_risk, const int max_spread) { if(FindNode(symbol,timeframe)!=NULL) { Print("Duplicate portfolio node rejected: ",symbol); return(false); } CStrategyNode *node=new CStrategyNode; if(node==NULL) return(false); if(!node.Initialize(symbol,timeframe,magic,allow_trading, signal_threshold,risk_percent,stop_points, reward_risk,max_spread)) { delete node; return(false); } if(!m_nodes.Add(node)) { delete node; return(false); } return(true); }
AddNode() forms a three-stage transaction: allocate, initialize, and insert. Ownership remains local during the first two stages, so either failure branch must delete the pointer. The successful Add() call is the commit point. From then onward, deletion must pass through CArrayObj. Keeping that boundary explicit prevents both leaks before insertion and double deletion after insertion.
Before the final Add(), node is locally owned and both failure branches delete it. After Add() succeeds, m_nodes owns the pointer. In the complete attachment, RemoveNode() calls Delete(), while the manager's Shutdown() first invokes each node's Shutdown() and then calls Clear(). With FreeMode enabled, Delete() and Clear() destroy their affected objects.
This division avoids double deletion: the manager may request node-level shutdown, but it does not directly delete a pointer already owned by CArrayObj. The source establishes that intended path. A dedicated destruction log or memory-analysis run would still be required to turn the ownership explanation into runtime evidence for removal, append failure, and full shutdown.
Timer-Driven Event Routing
A chart's OnTick event is driven by its attached symbol. That is an incomplete trigger for a portfolio because another managed symbol may update while the host chart is quiet. The EA therefore creates a timer and routes OnTimer() to ProcessAll(). The timer makes processing calls independent of host-chart ticks; it does not guarantee synchronized history, current quotes, or ready series for every managed instrument.
//+------------------------------------------------------------------+ //| Routes each timer event through one portfolio-wide cycle | //+------------------------------------------------------------------+ void OnTimer(void) { g_portfolio.ProcessAll(); Comment(g_portfolio.Summary()); }
OnTimer() remains deliberately small because event routing is not portfolio logic. It asks the manager to attempt one pass over the collection and then publishes the resulting snapshot with Comment(). Position refreshes still arrive through the trade-transaction path, while each node controls its own completed-bar gate. This separation lets us change the timer interval or chart presentation without rewriting signal evaluation or ownership behavior.
MetaTrader 5 maintains one timer per program. When a timer event is already queued or being processed, another timer event is not added to that program's queue. Every node still needs its own completed-bar gate because the timer interval is much shorter than the strategy timeframe.
OnTradeTransaction() asks all nodes to refresh their position view after terminal trading activity. OnDeinit() kills the timer before clearing the collection, ensuring no new portfolio cycle is requested during shutdown.
Operating the Expert Advisor
Download MQL5.zip, open the terminal's Data Folder, and extract the archive there. Merge the archive's top-level MQL5 folder with the existing MQL5 folder. This places ReusablePortfolioManagerEA.mq5 under MQL5\Experts\StandardLibraryExplorer\Part17\ and places PortfolioManager.mqh and StrategyNode.mqh under MQL5\Include\StandardLibraryExplorer\Part17\. The EA imports the manager through <StandardLibraryExplorer\Part17\PortfolioManager.mqh>. Compile ReusablePortfolioManagerEA.mq5 in MetaEditor, then attach it to any chart. The attached chart supplies the program instance, while InpSymbols defines the managed portfolio.
| Input | Default | Purpose |
|---|---|---|
| InpSymbols | EURUSD,GBPUSD,USDJPY,AUDUSD | Comma-separated list used to create nodes |
| InpTimeframe | PERIOD_H1 | Common processing timeframe for this installment |
| InpTimerSeconds | 1 | Portfolio routing interval |
| InpSignalThreshold | 0.00050 | Closed-bar return threshold |
| InpRiskPercentPerNode | 0.25 | Equity risk assigned to one new position |
| InpStopLossPoints | 300 | Requested initial stop distance |
| InpRewardRisk | 1.50 | Take profit distance relative to the stop |
| InpBaseMagic | 2026081700 | Base of the deterministic node magic sequence |
| InpMaxSpreadPoints | 30 | Maximum spread for a new position |
| InpAllowTrading | false | Explicit permission for all nodes to submit trade requests |
The parser trims spaces and continues after an invalid symbol. At least one node must be added successfully for initialization to continue. The chart summary displays node count, timer passes, failed node-processing attempts, and one status line per symbol-timeframe node.

Fig. 3. Multi-symbol positions during the trade-enabled test
Figure 3 records the trade-enabled test during Strategy Tester visualization. The chart comment shows four configured symbol-timeframe nodes. EURUSD, GBPUSD, and AUDUSD each hold a BUY position, while USDJPY remains without a position. The separate symbol tabs show that the host EA is coordinating several markets from one program instance.
The current signal for every node is NEUTRAL, yet three earlier BUY positions remain open. This observation is consistent with ApplySignal(), where a neutral state blocks a new entry without automatically closing an existing position. The visible volume, stop loss, take profit, and “Portfolio node” comments show that the test reached multi-symbol execution. A single frame cannot prove magic-number uniqueness, ownership isolation, risk-sizing accuracy, event-routing correctness, or profitability; those properties require logs and targeted scenarios.
Testing Results and Remaining Checks
I compiled the three Part 17 source files successfully in MetaTrader 5 Build 6140. I then separated testing into monitoring and execution runs so that the master permission gate could be verified independently from the strategy behavior.
Trading-Disabled Monitoring Run
The first test used EURUSD H1 as the host chart, Deriv-Demo history, OHLC bar-state generation, an initial deposit of 10,000 USD, and leverage of 1:100. I kept InpAllowTrading=false. The tester synchronized EURUSD, GBPUSD, USDJPY, and AUDUSD, initialized all four nodes, and explicitly reported that trading was disabled:
2026.09.03 19:11:29.604 successfully initialized 2026.09.03 19:11:29.668 EURUSD,H1 (Deriv-Demo): OHLC bar states generating. OnTick executed on the bar begin only 2026.09.03 19:11:29.668 InpSymbols=EURUSD,GBPUSD,USDJPY,AUDUSD 2026.09.03 19:11:29.668 InpTimeframe=16385 2026.09.03 19:11:29.668 InpTimerSeconds=1 2026.09.03 19:11:29.668 InpSignalThreshold=0.0005 2026.09.03 19:11:29.668 InpAllowTrading=false 2026.09.03 19:11:30.059 2024.01.01 00:00:00 Portfolio initialized with 4 nodes. Trading=DISABLED
The journal confirms initialization with InpAllowTrading=false and shows that the four nodes were created after symbol synchronization. Source inspection shows that ApplySignal() returns before order submission when m_allow_trading is false. However, this excerpt does not contain a complete order or deal history, so it supports the monitoring configuration without independently proving that no trade request occurred during the entire run.
Trade-Enabled Execution Run
For the second run, I set InpAllowTrading=true. Figure 3 shows open BUY positions on three managed symbols while USDJPY remains flat. This is evidence that one EA instance reached multi-symbol execution. It does not, by itself, verify risk calculations, magic-number uniqueness, foreign-position protection, or node recovery. Those claims remain separate test objectives.
| Scenario | Action | Status and required evidence |
|---|---|---|
| Trading disabled | Keep InpAllowTrading false | Partially observed: initialization and disabled state logged; a complete orders/deals report is still required to prove zero requests |
| Several valid symbols | Use the default list | Observed: four symbol-timeframe nodes initialized; uniqueness of their magic numbers was not independently logged |
| Invalid symbol | Insert an unavailable name | Planned: capture the rejection and continued processing of valid nodes |
| Duplicate symbol | Repeat one symbol | Planned: capture rejection of the repeated symbol-timeframe identity |
| Missing history | Observe a newly selected symbol before synchronization completes | Planned: log the unavailable series, other node activity, and later recovery |
| Completed-bar gate | Observe several timer cycles inside one H1 bar | Planned: compare timer-pass count with per-node evaluation timestamps |
| Foreign position | Create a position with another magic number | Planned: record the foreign ticket and the node's ownership-block message |
| Opposite owned position | Reach a signal opposite the node position | Planned: record the owned ticket, close request, result code, and absence of same-pass reversal |
| Shutdown | Remove the EA | Planned: add lifecycle diagnostics for node Shutdown(), destructor calls, Delete(), and Clear() |
Record the MetaTrader 5 build and the full test context: broker symbols, margin mode, host chart, timeframe, interval, modeling mode, spread, commission, slippage, and inputs. Multi-symbol Strategy Tester runs require synchronized history for every requested symbol. The test should separate lifecycle correctness from profitability.
Limitations
The momentum rule is an architectural demonstrator, not evidence of a trading advantage. It uses two completed bars and does not include confirmation, regime classification, or a portfolio-level exposure cap. The shared absolute return threshold also has different economic and volatility meanings across EURUSD, USDJPY, and other instruments.
Every node receives the same timeframe and risk percentage. A four-node portfolio can therefore request up to four times the per-node allocation when signals appear together. Part 19 of the planned roadmap will introduce a portfolio risk manager that considers aggregate exposure before individual nodes submit orders.
The common stop distance is expressed in symbol points. Equal point counts do not imply equal volatility distance across instruments. Although the volume estimate uses tick size and tick value, production sizing should validate broker properties and margin requirements for each instrument and account.
FindNode() uses linear search and the collection is homogeneous by convention. These choices are appropriate for a small educational portfolio. Larger plugin-based systems can implement comparison, sorting, typed wrappers, and indexed lookup.
The conservative foreign-position rule blocks a node whenever another strategy owns a position on the same symbol. This protects netting accounts but may be stricter than necessary on hedging accounts. A later trade-reconciliation module can distinguish tickets and ownership more precisely.
Timer-driven routing removes dependence on host-chart ticks, but it does not make every series ready. Nodes must continue treating zero timestamps, incomplete CopyRates() results, absent quotes, and trade-session restrictions as temporary or terminal conditions.
The current host refreshes position state after initialization and after trade transactions, but the article does not include a dedicated launch-with-existing-positions result. Test that scenario before relying on startup synchronization in a live portfolio.
Conclusion
We moved from a single-symbol EA to a reusable portfolio infrastructure that makes the desired guarantees explicit. CStrategyNode encapsulates all symbol-specific configuration and runtime state (bar gate, signal, magic, trade and position view). CPortfolioManager provides lifecycle coordination only — it does not implement strategy rules — and CArrayObj with FreeMode(true) enforces a predictable ownership boundary: create (local) → Initialize() → Add() (commit ownership) → Delete()/Clear() (collection-controlled destruction).
The practical outcomes are concrete and verifiable: a three-file scaffold (host EA + PortfolioManager + StrategyNode) that can create and remove N nodes from an input list, route timer events to ProcessAll(), maintain independent completed-bar gates, count timer cycles and failed node cycles, and prevent double deletions or leaks. Key operational checks are: nodes initialized, timer cycles incrementing, failed_cycles reflecting transient history or permission failures (not whole-program crashes), trading disabled when InpAllowTrading=false, and proper shutdown (timer stopped then Clear()).
This structure is ready to extend: add indicator caching, portfolio-level risk aggregation, trade reconciliation, and persistent recovery — but always preserve the ownership boundary and test lifecycle correctness independently from any claim of profitability.
Key Lessons
- One strategy object can replace a repeated set of symbol-specific globals.
- CArrayObj can own objects derived from CObject and release them through FreeMode.
- Ownership transfers only after Add() succeeds.
- Each symbol-timeframe node needs an independent completed-bar gate and failure state.
- A timer triggers portfolio passes independently of host-chart ticks, but it does not guarantee series readiness.
- A reproducible host-generated magic sequence is not the same as manager-enforced global uniqueness.
- Symbol and magic-number checks protect positions from unrelated strategies.
- Monitoring should remain useful when trading permission is disabled.
Attachments
I prepared one merge-ready attachment named MQL5.zip. Its top-level MQL5 folder mirrors the terminal Data Folder. The table explains each source component inside the archive and its installation location.
| Component | Type | Folder structure inside MQL5.zip | Responsibility |
|---|---|---|---|
| ReusablePortfolioManagerEA.mq5 | Expert Advisor | MQL5\Experts\StandardLibraryExplorer\Part17\ReusablePortfolioManagerEA.mq5 | Parses the symbol list and routes initialization, timer, trade-transaction, and shutdown events to the portfolio manager |
| PortfolioManager.mqh | Include class | MQL5\Include\StandardLibraryExplorer\Part17\PortfolioManager.mqh | Owns the CArrayObj collection and coordinates node creation, processing, lookup, removal, reporting, and cleanup |
| StrategyNode.mqh | Include class | MQL5\Include\StandardLibraryExplorer\Part17\StrategyNode.mqh | Encapsulates one symbol’s bar state, signal, risk calculation, trade permission, position ownership, execution, and diagnostics |
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.
Adaptive Trade Management in MQL5: Building a Broker-Aware, Deterministic, and Explainable Exit Engine
Online False Discovery Rate Control: Putting an Error Budget on a Live Signal Stream
Agent Based Market Simulation: Growing a Market From Rules, and Using It to Catch an Overfitted Strategy
Neural Networks in Trading: A Spiking-Semantic Approach to Spatiotemporal Identification (Key Components)
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use