The MQL5 Standard Library Explorer (Part 18): Building a Multi-Symbol Indicator Cache
In Part 17 of the MQL5 Standard Library Explorer, we replaced duplicated multi-symbol variables with independently owned strategy nodes coordinated by a portfolio manager. That structure exposes the next practical problem. In multi-symbol Expert Advisors, indicator objects are frequently created and managed inside every strategy module: each consumer repeats the same handle creation, BarsCalculated() checks, Refresh() calls, buffer reads, error handling, and release logic. This is acceptable while requests remain unique, but when several nodes request the same indicator type, symbol, timeframe, and inputs, it becomes inefficient and error-prone. Duplicate calculations, repeated readiness code, and unclear resource ownership make safe shutdown and partial initialization difficult.
This article implements a shared indicator cache that centralizes creation, readiness checking, buffer refresh, lifetime ownership, and final release. A consumer describes the indicator it needs; for the supported MA and ATR objects, the cache builds a key from every parameter accepted by the corresponding builder, returns an existing entry on a hit or creates one on a miss, applies the BarsCalculated()+Refresh()+GetData() readiness boundary, and manages lifetime with reference counting and a Release(entry&) contract. Strategy nodes keep their independent signal and position state, but they acquire and return cache entries instead of managing indicators directly. The result is a predictable, reusable resource layer suitable for adding future signal, risk, reporting, and visualization modules.
Contents
- Why an Indicator Cache
- Cache Architecture
- Defining Indicator Identity for MA and ATR
- Building Cache Entries
- Creating Standard Library Indicators
- Acquiring and Reusing Entries
- Reading Ready Indicator Data
- Reference Counting and Release
- Connecting Cache-Backed Strategy Nodes
- Coordinating the Cached Portfolio
- Operating the Expert Advisor
- Testing and Verification
- Limitations
- Conclusion
- Key Lessons
- Attachments
Why an Indicator Cache
A multi-symbol Expert Advisor (EA) often creates indicator handles inside every strategy module. That approach works while every request is unique. It becomes wasteful and difficult to maintain when several consumers request the same indicator type, symbol, timeframe, and inputs. Each module then repeats creation checks, readiness checks, buffer access, error handling, and release logic.
A cache turns indicator access into a service. The consumer describes the indicator it needs. The cache converts that description into a complete key and searches its entry collection. A matching entry is reused; a missing entry is created. Consumers return their references during shutdown, and the final release removes the entry.
| Without a cache | With a cache |
|---|---|
| Every module creates and releases indicators directly | The cache owns indicator creation and release |
| Matching requests can create duplicate resources | Matching keys return the same cache entry |
| Readiness logic is repeated | Every read passes through one readiness boundary |
| Ownership is distributed across consumers | References identify active consumers |
The objective is not merely to reduce the number of handles. We are building a predictable resource lifecycle that can support future signal, risk, reporting, and visualization modules.
Cache Architecture
The package contains four connected source modules. IndicatorCache.mqh defines the cache entry and cache service. CachedStrategyNode.mqh consumes cached indicators. IndicatorPortfolioManager.mqh owns the cache and the strategy-node collection. MultiSymbolIndicatorCacheEA.mq5 provides the terminal event handlers.

Fig. 1. Multi-symbol indicator cache components and ownership boundaries
Two CArrayObj collections appear in Fig. 1. The first owns strategy nodes. The second collection owns indicator cache entries. A cache entry owns one Standard Library CIndicator descendant. This layered ownership makes the shutdown order explicit: nodes return their references first, and the cache releases any remaining entries afterward.
| Class | Owns | Main responsibility |
|---|---|---|
| CIndicatorCacheEntry | One CIndicator-derived object | Readiness, refreshed data, status, and reference metadata |
| CIndicatorCache | CArrayObj of cache entries | Key lookup, acquisition, hit/miss counting, release, and deletion |
| CCachedStrategyNode | Three cache references | Closed-bar crossover signal and Average True Range risk distance |
| CIndicatorPortfolioManager | Cache and strategy-node collection | Creation, event routing, diagnostics, and ordered shutdown |
Defining Indicator Identity for MA and ATR
A cache can reuse an entry only if its identity includes every property that affects the output of the supported indicator. The symbol and timeframe are insufficient. Two moving averages on EURUSD H1 remain different when they use different periods, shifts, methods, or settings for the price field. In this implementation, the MA and ATR builders define complete identities for the parameters accepted by CiMA and CiATR; a future indicator requires its own parameter audit.
The moving-average key therefore contains:
- indicator type,
- symbol,
- timeframe,
- averaging period,
- horizontal shift,
- averaging method, and
- the price field used by the indicator.
//+------------------------------------------------------------------+ //| Builds a complete identity key for a moving average | //+------------------------------------------------------------------+ string CIndicatorCache::MAKey(const string symbol, const ENUM_TIMEFRAMES timeframe, const int period, const int shift, const ENUM_MA_METHOD method, const ENUM_APPLIED_PRICE applied) const { //--- Encode all moving-average parameters into one unique string return(StringFormat("MA|%s|%d|%d|%d|%d|%d",symbol,(int)timeframe, period,shift,(int)method,(int)applied)); }
Average True Range (ATR) needs only the indicator type, symbol, timeframe, and period. The string keys are readable in the chart summary, which helps us verify whether two requests should match. A production framework could replace strings with structured keys, but the identity requirements would remain the same.
The key-building function is the cache's identity boundary. Its arguments arrive in typed form, but StringFormat converts them into one deterministic lookup value. Including the enumeration values as integers prevents display names from influencing equality. If we later add another moving-average parameter, we must include it in both MAKey and AcquireMA. Otherwise, two distinct calculations may map to the same entry.
Building Cache Entries
CIndicatorCacheEntry derives from CObject so that CArrayObj can own it. The entry combines an indicator pointer with the information needed to manage it.
//+------------------------------------------------------------------+ //| Owns one Standard Library indicator and its cache metadata | //+------------------------------------------------------------------+ class CIndicatorCacheEntry : public CObject { private: string m_key; string m_symbol; ENUM_TIMEFRAMES m_timeframe; ENUM_CACHE_INDICATOR_TYPE m_type; CIndicator *m_indicator; int m_references; int m_requests; string m_status; public: CIndicatorCacheEntry(void); ~CIndicatorCacheEntry(void); bool CreateMA(const string key, const string symbol, const ENUM_TIMEFRAMES timeframe, const int period, const int shift, const ENUM_MA_METHOD method, const ENUM_APPLIED_PRICE applied); bool CreateATR(const string key, const string symbol, const ENUM_TIMEFRAMES timeframe, const int period); void AddReference(void); int RemoveReference(void); bool Value(const int shift,double &value); bool Ready(const int minimum_bars); void Release(void); string Key(void) const { return(m_key); } string Symbol(void) const { return(m_symbol); } ENUM_TIMEFRAMES Timeframe(void) const { return(m_timeframe); } ENUM_CACHE_INDICATOR_TYPE IndicatorType(void) const { return(m_type); } int References(void) const { return(m_references); } int Requests(void) const { return(m_requests); } int Handle(void) const; string Status(void) const { return(m_status); } string Summary(void) const; };
m_references records how many consumers currently hold the entry. m_requests records cumulative acquisitions, including cache hits. The distinction lets the chart summary show both current ownership and historical reuse.
The entry stores a CIndicator pointer because CIndicator is the common Standard Library base for CiMA, CiATR, and many other technical indicator classes. The cache can therefore use the same Handle, BarsCalculated, Refresh, GetData, and FullRelease interface after the specialized object has been created.
This class separates resource ownership from resource use. The strategy node never needs to know whether the stored pointer addresses CiMA or CiATR; it works through the common entry interface. At the same time, m_references answers a lifetime question, while m_requests answers a diagnostic question. Keeping those counters separate lets you distinguish an entry that has many active consumers from one that has simply been requested many times during its history.
Creating Standard Library Indicators
The first supported type is CiMA. CreateMA allocates the specialized object, calls its Create method, and transfers it to the base pointer only after creation succeeds.
//+------------------------------------------------------------------+ //| Creates and owns a moving-average indicator | //+------------------------------------------------------------------+ bool CIndicatorCacheEntry::CreateMA(const string key, const string symbol, const ENUM_TIMEFRAMES timeframe, const int period, const int shift, const ENUM_MA_METHOD method, const ENUM_APPLIED_PRICE applied) { //--- Refuse to overwrite an existing entry or accept invalid arguments if(m_indicator!=NULL || key=="" || symbol=="" || period<1) return(false); //--- Allocate a new moving-average indicator object CiMA *indicator=new CiMA; if(indicator==NULL) return(false); //--- Attach the indicator to the requested symbol and timeframe if(!indicator.Create(symbol,timeframe,period,shift,method,(int)applied)) { delete indicator; return(false); } //--- Adopt the indicator and record its initial reference count m_key=key; m_symbol=symbol; m_timeframe=timeframe; m_type=CACHE_INDICATOR_MA; m_indicator=indicator; m_references=1; m_requests=1; m_status="CREATED"; return(true); }
The second type is CiATR. Its creation signature needs the symbol, timeframe, and averaging period. Both paths delete an unowned object after creation failure. Once m_indicator receives the pointer, the cache entry owns it.
Release calls FullRelease, deletes the Standard Library object, sets the pointer to NULL, resets the reference count, and changes the status to RELEASED. The cache-entry destructor calls Release, so CArrayObj deletion reaches the underlying terminal indicator resource.
CreateMA behaves like a small transaction. Validation and indicator creation occur before m_indicator receives the pointer. Therefore, an invalid argument or failed Create call leaves the cache entry empty and safe to destroy. Only after the terminal accepts the indicator configuration does the function commit the key, symbol, timeframe, type, and counters. This ordering prevents a half-configured entry from appearing valid to a later cache lookup.
Acquiring and Reusing Entries
AcquireMA and AcquireATR follow the same workflow. They build a key, search the entry collection, return a hit when the key already exists, or create a new entry after a miss.

Fig. 2. Cache acquisition, reuse, and release workflow
//+------------------------------------------------------------------+ //| Returns a reused or newly created moving-average entry | //+------------------------------------------------------------------+ CIndicatorCacheEntry *CIndicatorCache::AcquireMA(const string symbol, const ENUM_TIMEFRAMES timeframe, const int period, const int shift, const ENUM_MA_METHOD method, const ENUM_APPLIED_PRICE applied) { //--- Reuse an existing entry when its identity is already cached const string key=MAKey(symbol,timeframe,period,shift,method,applied); const int found=FindIndex(key); if(found>=0) { CIndicatorCacheEntry *entry=(CIndicatorCacheEntry*)m_entries.At(found); if(entry!=NULL) { //--- Increment the reference count and record a cache hit entry.AddReference(); m_hits++; } return(entry); } //--- Allocate a new entry when no matching cached entry exists CIndicatorCacheEntry *entry=new CIndicatorCacheEntry; if(entry==NULL) return(NULL); //--- Create and store the moving-average entry in the cache if(!entry.CreateMA(key,symbol,timeframe,period,shift,method,applied)) { delete entry; return(NULL); } if(!m_entries.Add(entry)) { delete entry; return(NULL); } //--- Record the cache miss and return the newly created entry m_misses++; return(entry); }
A hit returns the existing pointer and increments both its active reference count and cumulative request count. A miss allocates CIndicatorCacheEntry, creates the requested Standard Library indicator, and adds the entry to the owning collection. If creation or Add fails, the unowned entry is deleted immediately.
The EA calls ProbeMAReuse after the first strategy node is created. The probe requests the same fast moving average and immediately releases it. It does not create another strategy node or retain an extra reference. Its purpose is to make the hit path visible in the initial cache diagnostics.
The acquisition path also defines when ownership changes. Finding a key is not enough: the retrieved pointer must be valid before AddReference records another consumer. On a miss, the new entry remains local until both CreateMA and CArrayObj::Add succeed. The cache increments m_misses only after that transfer. Consequently, the counters describe usable cache results rather than allocation attempts that were rolled back.
Reading Ready Indicator Data
A valid indicator object does not guarantee that its buffers already contain the requested historical index. CIndicatorCacheEntry::Ready checks the handle, verifies the calculated bar count, and refreshes the indicator data before any read.
//+------------------------------------------------------------------+ //| Checks calculation readiness and refreshes recent buffer data | //+------------------------------------------------------------------+ bool CIndicatorCacheEntry::Ready(const int minimum_bars) { //--- Reject entries that have no valid indicator handle if(m_indicator==NULL || m_indicator.Handle()==INVALID_HANDLE) { m_status="INVALID HANDLE"; return(false); } //--- Wait until the indicator has calculated enough bars if(m_indicator.BarsCalculated()<minimum_bars) { m_status="WAITING FOR BARS"; return(false); } //--- Refresh the indicator buffers and mark the entry as ready m_indicator.Refresh(); m_status="READY"; return(true); }
The documentation for CIndicator::GetData states that Refresh should be called before recent data are accessed. The current Ready implementation invokes Refresh but does not test a separate success result. Its READY status therefore means that the handle is valid, the required number of bars is calculated, and a refresh was requested; it does not independently confirm refresh success.
Value adds the data-level checks that follow readiness. It initializes the output defensively, rejects negative shifts, reads buffer zero, and refuses EMPTY_VALUE or a number that fails MathIsValidNumber. This extract makes the read contract visible rather than asking you to infer it from the class description.
//+------------------------------------------------------------------+ //| Reads one value from buffer zero after readiness validation | //+------------------------------------------------------------------+ bool CIndicatorCacheEntry::Value(const int shift,double &value) { //--- Initialize the output and reject out-of-range shifts value=EMPTY_VALUE; if(shift<0 || !Ready(shift+1)) return(false); //--- Read the requested value from buffer zero of the indicator value=m_indicator.GetData(0,shift); if(value==EMPTY_VALUE || !MathIsValidNumber(value)) { m_status="DATA UNAVAILABLE"; return(false); } return(true); }
The strategy reads shifts 1 and 2 rather than shift 0. These indices represent completed bars after a new bar has opened. The node still maintains its independent completed-bar gate, so the timer cannot reevaluate the same crossover repeatedly.
Ready reports two conditions explicitly. WAITING FOR BARS tells the node to postpone evaluation while history is still being calculated, and INVALID HANDLE identifies a broken resource. Because Value calls this gate before GetData, every strategy receives the same validation behavior. A production extension should add an explicit refresh-result check if the selected Standard Library API exposes a result that can be tested reliably.
Reference Counting and Release
A consumer must not delete a cache entry directly. It returns the pointer to CIndicatorCache::Release. The cache locates that pointer in its own collection, decrements its reference count, clears the caller's pointer, and deletes the collection element only when no consumer remains. This contract protects the implemented node paths, but it cannot prevent another consumer from losing a pointer, failing to release it, or attempting an unbalanced release.
//+------------------------------------------------------------------+ //| Releases one reference and deletes the final unreferenced entry | //+------------------------------------------------------------------+ bool CIndicatorCache::Release(CIndicatorCacheEntry *&entry) { //--- Treat a null pointer as an already-released entry if(entry==NULL) return(true); //--- Locate the entry within the collection for(int i=0;i<m_entries.Total();i++) { if(m_entries.At(i)!=entry) continue; //--- Remove the reference and delete the entry when unused const int remaining=entry.RemoveReference(); entry=NULL; if(remaining==0) return(m_entries.Delete(i)); return(true); } //--- The entry was not found in the collection entry=NULL; return(false); }
Because m_entries uses FreeMode(true), Delete destroys the cache entry. Its destructor then calls FullRelease on the owned CIndicator object. This creates a complete chain:
- The strategy node returns its reference.
- The cache decrements the reference count.
- The final release removes the entry from CArrayObj.
- The entry destructor releases and deletes the indicator object.
Shutdown order is essential. Strategy nodes must be destroyed before the cache. Otherwise, a node could attempt to return a pointer to a cache that no longer owns the entry.
Setting the caller's entry pointer to NULL is part of the release contract; it is not cosmetic cleanup. It prevents the consumer from retaining an address after the final CArrayObj::Delete may have destroyed the object. Returning true for an already-null pointer also makes repeated cleanup safe during partial initialization and normal destruction. The manager can therefore unwind several failure paths without needing a separate flag for every acquired indicator.
Connecting Cache-Backed Strategy Nodes
Each CCachedStrategyNode receives a pointer to the shared cache during initialization. It acquires a fast exponential moving average (EMA), a slow EMA, and ATR. The node stores entry pointers rather than creating indicators itself.
//+------------------------------------------------------------------+ //| Acquires three indicators and initializes the symbol node | //+------------------------------------------------------------------+ bool CCachedStrategyNode::Initialize(CIndicatorCache *cache, const string symbol, const ENUM_TIMEFRAMES timeframe, const ulong magic, const int fast_period, const int slow_period, const int atr_period, const bool allow_trading, const double risk_percent, const double atr_multiplier, const int max_spread) { //--- Reject invalid arguments or a missing cache reference if(cache==NULL || symbol=="" || magic==0 || fast_period<1 || slow_period<=fast_period || atr_period<1 || risk_percent<=0.0 || atr_multiplier<=0.0 || max_spread<0) return(false); //--- Make sure the symbol is visible in the Market Watch if(!SymbolSelect(symbol,true)) return(false); //--- Store the node configuration m_cache=cache; m_symbol=symbol; m_timeframe=timeframe; m_magic=magic; m_allow_trading=allow_trading; m_risk_percent=risk_percent; m_atr_multiplier=atr_multiplier; m_max_spread=max_spread; //--- Acquire the fast EMA from the shared indicator cache m_fast_ma=m_cache.AcquireMA(symbol,timeframe,fast_period,0, MODE_EMA,PRICE_CLOSE); if(m_fast_ma==NULL) { Shutdown(); return(false); } //--- Acquire the slow EMA from the shared indicator cache m_slow_ma=m_cache.AcquireMA(symbol,timeframe,slow_period,0, MODE_EMA,PRICE_CLOSE); if(m_slow_ma==NULL) { Shutdown(); return(false); } //--- Acquire the ATR used for stop distance from the shared cache m_atr=m_cache.AcquireATR(symbol,timeframe,atr_period); if(m_atr==NULL) { Shutdown(); return(false); } //--- Configure the trade object with the node magic and deviation m_trade.SetExpertMagicNumber(m_magic); m_trade.SetDeviationInPoints(20); //--- Select a filling type supported by the symbol if(!m_trade.SetTypeFillingBySymbol(m_symbol)) { Shutdown(); return(false); } //--- Store the current bar time and mark the node ready m_last_bar=iTime(m_symbol,m_timeframe,0); m_initialized=true; m_enabled=true; m_status="READY"; return(true); }
Initialization stops and returns every acquired reference if a later acquisition fails. This prevents partial initialization from leaving an entry with a reference count that no active node owns.
EvaluateSignal reads the two completed values from both moving averages and one completed ATR value. A bullish crossover occurs when the fast EMA is above the slow EMA at shift 1 and was not above it at shift 2. The bearish condition reverses those comparisons. ATR defines the stop distance when trading is explicitly enabled.
Shutdown disables the node and returns fast MA, slow MA, and ATR references exactly once. Release sets each caller pointer to NULL, so a later destructor call remains safe.
Initialization demonstrates dependency ordering inside one node. The cache address and symbol configuration are stored first, then the fast EMA, slow EMA, and ATR references are acquired in sequence. Any failure calls Shutdown, which returns the references already obtained. Trading settings are applied only after all three resources exist. When Initialize returns true, the manager can rely on a complete node rather than testing each dependency again during every timer cycle.
Coordinating the Cached Portfolio
CIndicatorPortfolioManager owns the cache and a CArrayObj of strategy nodes. AddNode creates one node per symbol-timeframe identity and passes the same cache address to every successful node.
//+------------------------------------------------------------------+ //| Creates one node and transfers its ownership to CArrayObj | //+------------------------------------------------------------------+ bool CIndicatorPortfolioManager::AddNode(const string symbol, const ENUM_TIMEFRAMES timeframe, const ulong magic, const int fast_period, const int slow_period, const int atr_period, const bool allow_trading, const double risk_percent, const double atr_multiplier, const int max_spread) { //--- Reject duplicate nodes for the same symbol and timeframe if(FindNode(symbol,timeframe)!=NULL) { Print("Duplicate cached node rejected: ",symbol); return(false); } //--- Allocate a new cached strategy node CCachedStrategyNode *node=new CCachedStrategyNode; if(node==NULL) return(false); //--- Initialize the node against the shared indicator cache if(!node.Initialize(&m_cache,symbol,timeframe,magic,fast_period, slow_period,atr_period,allow_trading,risk_percent, atr_multiplier,max_spread)) { delete node; return(false); } //--- Transfer the node ownership to the collection if(!m_nodes.Add(node)) { delete node; return(false); } return(true); }
Deleting a partially initialized node invokes its destructor, which returns any cache entries it already acquired. After Add succeeds, the node collection owns the object.
ProcessAll routes each timer cycle to every node and records isolated failures. Summary combines portfolio counters, cache entries, handles, references, requests, hit/miss totals, and symbol-level signals. Shutdown clears nodes first and the cache second.
AddNode creates a second ownership boundary around that initialized node. Before m_nodes.Add succeeds, the local pointer belongs to the function and must be deleted after failure. After insertion, CArrayObj owns it through FreeMode(true). This rule complements the cache's reference ownership: deleting an unaccepted node triggers its destructor and returns any indicator references, while deleting an accepted node through the collection performs the same cleanup during portfolio shutdown.
Operating the Expert Advisor
Extract MQL5.zip into the terminal's Data Folder and merge its MQL5 folder with the existing MQL5 folder. MultiSymbolIndicatorCacheEA.mq5 is installed under MQL5\Experts\StandardLibraryExplorer\Part18\. The header files (IndicatorPortfolioManager.mqh, CachedStrategyNode.mqh, and IndicatorCache.mqh) are installed under MQL5\Include\StandardLibraryExplorer\Part18\. The EA imports the manager through <StandardLibraryExplorer\Part18\IndicatorPortfolioManager.mqh>. Compile the EA in MetaEditor and attach it to any chart. The program selects and manages the symbols listed in InpSymbols.
| Input | Default | Purpose |
|---|---|---|
| InpSymbols | EURUSD,GBPUSD,USDJPY,AUDUSD | Comma-separated managed symbols |
| InpTimeframe | PERIOD_H1 | Timeframe included in every cache key |
| InpFastMAPeriod | 12 | Fast EMA averaging period |
| InpSlowMAPeriod | 26 | Slow EMA averaging period |
| InpATRPeriod | 14 | ATR averaging period |
| InpATRMultiplier | 2.00 | ATR multiple used for the stop distance |
| InpTimerSeconds | 1 | Portfolio processing interval |
| InpRiskPercentPerNode | 0.25 | Equity risk assigned to a new node position |
| InpBaseMagic | 2026081800 | Base for deterministic symbol ownership |
| InpMaxSpreadPoints | 30 | Maximum spread permitted for entry |
| InpAllowTrading | false | Explicit trade-execution permission |
Trading remains disabled by default. Monitoring mode still creates and refreshes the cache, processes completed bars, detects EMA crossovers, stores ATR values, counts hits and misses, and reports entry status. This makes resource behavior observable before execution is permitted.

Fig. 3. Cache diagnostics, node states, and open multi-symbol positions in the Strategy Tester
Figure 3 captures the cache and its consumers during a trading-enabled test. The summary reports 12 entries for four symbols because every node requests a fast exponential moving average (EMA), a slow EMA, and an Average True Range (ATR) object. Each visible entry is READY and has one active reference. The first symbol's fast-MA entry records two cumulative requests when ProbeMAReuse has run: one request from the node and one temporary probe request. The probe releases its temporary reference, so the active count returns to one. This snapshot is consistent with one retained entry per displayed key, but it does not prove that duplication is impossible in every failure, restart, or reinitialization path.
The node summary confirms that the shared service does not merge state across symbols. EURUSD, GBPUSD, USDJPY, and AUDUSD retain independent ATR values, signals, position states, and failure counters. Three nodes hold buy positions while USDJPY remains flat. The Trade tab confirms that those positions have different volumes and stop distances. This is consistent with per-symbol ATR conditions and risk-based volume calculation rather than one fixed order template applied to the entire portfolio.
The displayed floating loss is only the state of the open positions at the captured moment. It is not a performance conclusion. The screenshot shows that several nodes were consuming READY cache entries while maintaining different trading states at that moment. Lifecycle invariants still require targeted acquisition, release, failure, and shutdown tests.
Testing and Verification
I tested the EA on EURUSD H1 from January 1 to December 31, 2024, using the 1-minute OHLC modeling method. The portfolio contained EURUSD, GBPUSD, USDJPY, and AUDUSD. Trading was enabled, the fast and slow EMA periods were 12 and 26, the ATR period was 14, and the ATR multiplier was 2.0. The journal confirms successful initialization, synchronization of all four histories, market-order execution, and server-side stop-loss and take-profit exits.
The table below is a verification plan, not a completed-results matrix. The recorded run supports initialization and trade-flow observations. Reuse, failed acquisition, unready history, final release, and ordered shutdown require their listed targeted procedures and should not be treated as proven by the trade journal alone.
| Scenario | Procedure | Expected evidence |
|---|---|---|
| Initial misses | Start with one valid symbol | Three entries and three misses for fast MA, slow MA, and ATR |
| Reuse probe | Allow OnInit() to request the first fast MA again | One hit; the retained reference count returns to one |
| Several symbols | Use the default portfolio | Three distinct keys per symbol |
| Unready history | Add a newly selected symbol before synchronization completes | WAITING FOR BARS or INDICATORS NOT READY without invalid access |
| Completed-bar gate | Observe many timer cycles inside one H1 bar | Each node evaluates the crossover once |
| Partial initialization | Force a later indicator acquisition to fail | Earlier references are returned by the node destructor |
| Final release | Remove or shut down the final consumer | Reference reaches zero and the entry is deleted |
| Ordered shutdown | Remove the EA | Nodes release references before remaining cache entries are cleared |
| Trading disabled | Keep InpAllowTrading false | Indicators and signals update without orders |
Recorded Tester Evidence
The following extract keeps the decisive events in chronological order. It begins with cache-backed portfolio initialization, then shows a long EURUSD order, its stop-loss exit, short EURUSD and GBPUSD orders, two take-profit exits, and later long orders across three symbols.
2024.01.01 00:00:00 Cached portfolio initialized with 4 nodes. Trading=ENABLED 2024.01.04 10:00:00 CTrade::OrderSend: market buy 0.12 EURUSD sl: 1.09479 tp: 1.09999 [done at 1.09687] 2024.01.04 11:59:58 stop loss triggered #2 buy 0.12 EURUSD 1.09687 sl: 1.09479 tp: 1.09999 [#3 sell 0.12 EURUSD at 1.09479] 2024.01.05 07:00:00 CTrade::OrderSend: market sell 0.15 EURUSD sl: 1.09423 tp: 1.09033 [done at 1.09267] 2024.01.05 09:00:00 CTrade::OrderSend: market sell 0.11 GBPUSD sl: 1.26833 tp: 1.26274 [done at 1.26609] 2024.01.05 13:59:58 take profit triggered #4 sell 0.15 EURUSD 1.09267 sl: 1.09423 tp: 1.09033 [#6 buy 0.15 EURUSD at 1.09033] 2024.01.05 13:59:58 take profit triggered #5 sell 0.11 GBPUSD 1.26609 sl: 1.26833 tp: 1.26274 [#7 buy 0.11 GBPUSD at 1.26274] 2024.01.05 16:00:00 CTrade::OrderSend: market buy 0.05 GBPUSD sl: 1.27072 tp: 1.28212 [done at 1.27528] 2024.01.05 17:00:00 CTrade::OrderSend: market buy 0.06 EURUSD sl: 1.09175 tp: 1.10165 [done at 1.09571] 2024.01.05 20:00:00 CTrade::OrderSend: market buy 0.06 AUDUSD sl: 0.66769 tp: 0.67726 [done at 0.67152]
The EURUSD orders use 0.12, 0.15, and 0.06 lots. These differences are consistent with the implemented ATR stop calculation and risk budget, but the journal alone cannot isolate their cause because equity, tick value, volume steps, symbol limits, and rounding also affect the final volume. The multi-symbol orders confirm that the manager processed several configured symbols during this test. Independence from host-chart ticks follows from the timer-based event path in the code, not from the trade log by itself.
This run confirms initialization and selected order-flow behavior under the stated period, symbols, inputs, and modeling method. It does not by itself verify every cache lifecycle path, profitability, or robustness. Before using the architecture beyond this lesson, record the account margin mode, spread, commission, slippage assumptions, and data quality. Repeat the test across out-of-sample periods and broker feeds. Multi-symbol tests also require synchronized history for every managed symbol.
Limitations
The cache supports CiMA and CiATR only. Adding CiRSI, CiBands, or a multi-buffer indicator requires a new key builder and creation method. The common Value method reads buffer zero; indicators with several meaningful buffers need an explicit buffer parameter or a specialized entry interface.
FindIndex performs a linear search through CArrayObj. This is clear and adequate for a small portfolio, but lookup cost grows with the number of unique indicator configurations. A later implementation can use sorted entries, comparison methods, or a separate index without changing consumer ownership.
Reference counting is manual. Every successful acquisition must have one matching release. The node destructor protects the implemented paths, but future consumers must follow the same contract. Release searches for the pointer in the cache collection, but it cannot prevent a consumer from losing its pointer or balance ownership across code that does not follow the contract. Forced Clear during shutdown releases remaining owned entries, yet that fallback does not make an unbalanced reference history correct. Reference counting also does not resolve cyclic ownership; this architecture avoids cycles by ensuring entries have no knowledge of consumers.
The EMA crossover is an educational consumer of the cache, not evidence of a trading advantage. The fixed 1.50 reward-to-risk ratio and the per-node allocation are deliberately simple. The recorded run demonstrates order flow and resource coordination, but it does not establish robustness. Aggregate exposure control remains the subject of the planned reusable risk-management installment.
Conclusion
We extended the portfolio manager with a centralized indicator service whose resource behavior can be inspected and tested. For the supported CiMA and CiATR configurations, the cache uses deterministic keys that encode the parameters accepted by their builders. It stores one CArrayObj-owned CIndicatorCacheEntry per successfully materialized key and exposes a small contract: Acquire returns a reused or newly created entry; Ready and Value apply the BarsCalculated()+Refresh()+GetData() read boundary; and Release(entry&) decrements active ownership, clears the caller's pointer, and deletes the entry when its reference count reaches zero. Hits, successful misses, cumulative requests, active references, and readiness states make the demonstrated paths observable. Correct shutdown still depends on consumers following the contract and on destroying nodes before clearing the cache.
As a consequence, the implemented strategy nodes no longer create, refresh, or delete terminal indicators directly. They consume shared entries while preserving their per-symbol state independently. The design can be extended through additional indicator-specific keys, explicit buffer selection, stronger refresh diagnostics, protected ownership wrappers, or alternative lookup indexes. Within the demonstrated paths, it centralizes duplicated resource work and makes ownership responsibilities explicit without claiming that one recorded run proves every lifecycle invariant.
Key Lessons
This article shows what the MQL5 Standard Library gives you when its classes are combined deliberately: reusable indicator objects, a common technical-indicator interface, an owning object collection, and predictable class-based cleanup. The lessons below connect each Library facility to the programming concept you learn and to its practical value when you build a multi-symbol trading system.
| Standard Library focus | Programming concept learned | Practical trading-system benefit |
|---|---|---|
| CIndicator: a common indicator abstraction | Polymorphism: CIndicator gives CiMA and CiATR a common readiness, refresh, and data-access surface while their creation parameters remain type-specific. | You can expand the EA with another CIndicator-derived study without rewriting every strategy node, making new trading ideas faster to integrate and easier to compare. |
| CiMA and CiATR: typed indicator objects | Resource identity: every parameter that changes a calculation must participate in the key used to decide whether an object can be shared. | A fast EMA, slow EMA, or ATR request cannot silently receive another configuration, so each symbol is analyzed with the indicator settings you intended. |
| CObject: a storable polymorphic base | Encapsulation: a CObject-derived wrapper combines the indicator pointer with identity, diagnostics, reference counts, and lifetime rules. | The strategy receives a controlled indicator service instead of handling fragile terminal resources directly, reducing resource-management faults during monitoring or trading. |
| CArrayObj: collection ownership | Collection ownership: CArrayObj stores cache entries and FreeMode(true) makes deletion follow removal from the owning collection. | One cache controls the lifetime of indicator resources across all symbols, reducing leaks and double deletion when the EA initializes partially or shuts down. |
| CIndicator data-access methods | A validation boundary: BarsCalculated, Refresh, and GetData are coordinated behind Ready and Value rather than scattered across consumers. | A node postpones its signal when its indicator data are not ready instead of trading from an invalid or incomplete buffer value. |
| CIndicator::FullRelease and object destruction | Symmetric lifetime management: every successful Acquire requires a matching Release, and the final release performs terminal and object cleanup. | Several symbol nodes can share a calculation without one node prematurely destroying an indicator that another node still needs. |
| CArrayObj::Clear and dependency order | Reverse dependency cleanup: consumer nodes are destroyed before the shared cache that serves them. | EA removal and failed initialization follow a predictable shutdown path rather than leaving nodes with references to an already destroyed service. |
| Object metadata around Library classes | Observability: wrapper metadata exposes keys, hits, successful misses, cumulative requests, active references, and readiness states. | You can diagnose whether nodes are reusing the intended indicators and whether a symbol is waiting for data before you interpret its signal or enable trading. |
| Composition of collections and indicator classes | Separation of shared services and local state: nodes share indicator infrastructure but retain their own symbol, bar gate, signal, ATR value, position view, and failures. | A data problem or neutral signal on one market does not need to overwrite the analytical state of the other managed markets. |
| Extending rather than replacing the Library | Extension by composition: the cache adds application-level identity and sharing while leaving indicator calculation to the tested Standard Library classes. | You can improve lookup, ownership guards, refresh diagnostics, or multi-buffer access without rebuilding MA and ATR calculations from scratch. |
Attachments
I prepared MQL5.zip as a merge-ready archive. Extract its MQL5 folder into the terminal's Data Folder. The archive preserves the separation between the host EA and the reusable headers:
| Component | Type | Installation path | Responsibility |
|---|---|---|---|
| MultiSymbolIndicatorCacheEA.mq5 | Expert Advisor | MQL5\Experts\StandardLibraryExplorer\Part18\ | Parses symbols and hosts timer and trade-transaction events |
| IndicatorPortfolioManager.mqh | Include class | MQL5\Include\StandardLibraryExplorer\Part18\ | Owns the cache and cache-backed node collection |
| CachedStrategyNode.mqh | Include class | MQL5\Include\StandardLibraryExplorer\Part18\ | Consumes cached fast MA, slow MA, and ATR entries |
| IndicatorCache.mqh | Include class | MQL5\Include\StandardLibraryExplorer\Part18\ | Creates, reuses, refreshes, reference-counts, and releases indicators |
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.
Controlling MetaTrader 5 from Telegram with Hermes and MCP
Neural Networks in Trading: A Spike-Semantic Approach to Spatiotemporal Identification (Conclusion)
Features of Experts Advisors
The Hypothesis of Randomness: Searching for Hidden Patterns in Price Series
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use