preview
Building a Broker-Agnostic Symbol Resolution Layer in MQL5

Building a Broker-Agnostic Symbol Resolution Layer in MQL5

MetaTrader 5Trading systems |
261 0
Christian Benjamin
Christian Benjamin

Contents


Introduction

Deploying the same Expert Advisor across multiple MetaTrader 5 brokers often fails not because of trading logic, but because instrument identifiers differ between terminals. The same market can appear as EURUSD, EURUSDm, EURUSD.fx, GOLD, XAUUSD.a, US30Cash, or US30 — and those differences break symbol selection (SymbolSelect), price reads (SYMBOL BID / SYMBOL ASK) and order submission. Failures can occur at initialization (symbols not selectable), in OnTick (quotes return zero), or at execution (OrderSend rejected), and they are frequently silent: the strategy appears correct but cannot operate.

To solve this we build a broker‑agnostic symbol abstraction layer that lets strategies use logical names (EURUSD, XAUUSD, US30) while the framework performs deterministic translation, terminal validation, caching and persistence. Success is defined and testable: Resolve("EURUSD") returns either a verified broker symbol (SymbolSelect succeeded and bid/ask are non-zero) or an empty result that is logged; discovered mappings are saved to CSV and reused after restart; and common operations (SymbolSelect/OrderSend/price reads) produce no mapping‑related errors. The pipeline is deterministic (description → prefix/suffix normalization → controlled substring scoring) with discovery treated as a controlled fallback and all decisions recorded for review.


Project Architecture

Before implementing the files, review the overall framework structure. Symbol abstraction cannot be solved with a single lookup function. It needs a sequence of stages that can discover symbols, store what we learn, resolve logical names at runtime, cache repeated requests, and verify the result inside the terminal.

To keep the design manageable, each responsibility is separated into its own component. That gives us a clear build order, easier testing, and a structure that can adapt when a broker introduces a different naming style.

The workflow begins with symbol discovery and ends with verified mappings that the trading logic can use safely. Along the way, the system preserves what it learns so that the same broker does not need to be rediscovered every time the EA starts.

Broker Symbol Problem

MetaTrader 5 does not enforce standardized instrument names across brokers. The same underlying market may appear under different identifiers such as EURUSD, EURUSD.fx, or EURUSDm. This variation applies across forex, indices, metals, energy, and cryptocurrency markets.

Direct symbol references inside an Expert Advisor create portability problems. When a symbol is renamed, the strategy does not fail because the logic is wrong. It fails because the symbol string no longer matches the broker’s terminal. That is why a separate abstraction layer is required.

The table below illustrates typical symbol naming variations used by different brokers for the same underlying instruments:

Logical Symbol Broker A  Broker B Broker C
EURUSD EURUSD EURUSDm EURUSD.fx
XAUUSD XAUUSD GOLD XAUUSD.a
US30 US30 DJ30 US30.cash
US100 NAS100 US100 USTEC
GBPUSD GBPUSD GBPUSDm GBPUSD.pro
BTCUSD BTCUSD BTCUSD.r XBTUSD

The mapping layer abstracts these broker-specific differences into a single, unified logical naming system, allowing the EA to work consistently across multiple brokers without hardcoding symbol strings.

Framework Objectives

The framework must discover mappings, persist them across sessions, resolve logical names into broker-specific identifiers, cache repeated results, and reject invalid mappings. It must also confirm that resolved symbols can be selected and priced successfully inside the terminal.

Component Structure

The system consists of five core modules:

  • CMappingStorage maintains discovered symbol mappings and persists them for reuse.
  • SymbolMapper resolves logical symbols into broker-specific identifiers using stored data and fallback discovery rules.
  • CResolutionCache stores previously resolved symbols so repeated requests do not repeat the same work.
  • HostVerification.mq5 validates mappings by checking symbol selection and market data access in a live terminal.
  • SymbolLookupBench.mq5 measures resolution latency under repeated use.

Resolution Pipeline

Symbol resolution starts when the EA asks for a logical instrument name such as EURUSD, XAUUSD, or US30.

The execution flow is straightforward:

  • Check the cache and the internal mapping table.
  • If a match exists, return the broker symbol immediately.
  • If no match exists, try lazy discovery against the terminal symbol list.
  • Apply the normalization rules to narrow the candidate list.
  • Persist the successful mapping and update the cache.

The resolved symbol is then passed to the verification layer, where HostVerification.mq5 checks that it can be selected and priced correctly.

Execution Cycle

The operational lifecycle of the framework can be summarized as:

discovery → resolution → caching → verification → persistence

This sequence allows the framework to adapt to broker-specific naming while maintaining efficient runtime access.


Building the Mapping Storage Layer

The mapping storage layer comes first because it gives the framework a place to remember what it has learned. Broker naming is inconsistent, so the system needs a module that can hold logical-to-broker relationships and preserve them across sessions.

To do that, we use CMappingStorage. It combines persistent storage with runtime discovery so the framework can work even when no mapping file exists yet.

The main idea is simple: once a valid mapping is found, the system should not have to discover it again every time the EA starts.

Internal Data Model

//+------------------------------------------------------------------+
//| Internal Mapping Storage                                         |
//+------------------------------------------------------------------+
private:
  string m_logicalNames[];
  string m_brokerNames[];
  string m_unknownLog[];

We keep mappings in parallel arrays. Each index represents one complete relationship.

m_logicalNames[i] stores the logical instrument name.

m_brokerNames[i] stores the broker-specific symbol.

This structure is small, direct, and easy to maintain. It is a good fit for Expert Advisors because it avoids unnecessary overhead.

Loading Layer (Persistence Recovery)

The loading phase restores mappings from CSV when a file is available. If the file cannot be opened, the system does not stop. It simply continues with an empty repository so discovery can take over.

//+------------------------------------------------------------------+
//| Load mappings from a CSV file                                    |
//+------------------------------------------------------------------+
bool CMappingStorage::LoadFromFile(const string filename)
  {
  //--- Clear any existing mappings before loading
  Reset();

  //--- Open the CSV file for reading (comma‑separated)
  int handle = FileOpen(filename, FILE_READ | FILE_TXT | FILE_CSV, ",");
  if(handle == INVALID_HANDLE)
    {
    Print("MappingStorage: Cannot open ", filename, ", error ", GetLastError());
    return(false);
    }

  //--- Skip the header line (assumes "logicalName,brokerName")
  FileReadString(handle);

  //--- Read each subsequent line until EOF
  while(!FileIsEnding(handle))
    {
    string logical = FileReadString(handle);
    string broker  = FileReadString(handle);

    //--- Trim any leading/trailing whitespace
    StringTrimLeft(logical);
    StringTrimRight(logical);
    StringTrimLeft(broker);
    StringTrimRight(broker);

    //--- Only add non‑empty pairs and avoid duplicates
    if(logical != "" && broker != "")
      {
      bool exists = false;
      for(int i = 0; i < ArraySize(m_logicalNames); i++)
        {
        if(m_logicalNames[i] == logical)
          {
          exists = true;
          break;
          }
        }
      if(!exists)
        {
        int sz = ArraySize(m_logicalNames);
        ArrayResize(m_logicalNames, sz + 1);
        ArrayResize(m_brokerNames, sz + 1);
        m_logicalNames[sz] = logical;
        m_brokerNames[sz] = broker;
        }
      }
    }

  FileClose(handle);
  Print("MappingStorage: Loaded ", TotalMappings(), " mappings from ", filename);
  return(true);
  }

That gives us continuity without requiring manual setup before the EA can run.

Generation Layer (Discovery Engine)

When no mapping file exists, the system enters discovery mode and builds mappings directly from the terminal symbol list.

//+------------------------------------------------------------------+
//| Generate heuristic mappings from terminal symbols                |
//+------------------------------------------------------------------+
bool CMappingStorage::GenerateAll(bool verbose = false)
  {
  //--- Start with a clean slate
  Reset();

  int total = SymbolsTotal(false);   // Only currently selected symbols
  if(total == 0)
    return(false);

  int added = 0;

  //--- Step 1: Attempt to match using SYMBOL_DESCRIPTION (most reliable)
  //--- This handles symbols like "EURUSD" where description contains "Euro vs USD"
  for(int i = 0; i < total; i++)
    {
    string brokerName = SymbolName(i, false);
    if(brokerName == "")
      continue;

    string logical = LogicalFromDescription(brokerName);
    if(logical != "")
      {
      bool exists = false;
      for(int j = 0; j < ArraySize(m_logicalNames); j++)
        {
        if(m_logicalNames[j] == logical)
          {
          exists = true;
          break;
          }
        }
      if(!exists)
        {
        int sz = ArraySize(m_logicalNames);
        ArrayResize(m_logicalNames, sz + 1);
        ArrayResize(m_brokerNames, sz + 1);
        m_logicalNames[sz] = logical;
        m_brokerNames[sz] = brokerName;
        added++;
        if(verbose)
          Print("  Desc match: ", logical, " -> ", brokerName);
        }
      }
    }

  //--- Step 2: Strip prefixes/suffixes for remaining symbols
  for(int i = 0; i < total; i++)
    {
    string brokerName = SymbolName(i, false);
    if(brokerName == "")
      continue;

    string logical = StripPrefix(brokerName);
    logical = StripSuffix(logical);

    //--- Only accept if the stripped name is at least 2 characters
    //--- and different from the original (i.e., something was stripped)
    if(logical != "" && logical != brokerName && StringLen(logical) >= 2)
      {
      bool exists = false;
      for(int j = 0; j < ArraySize(m_logicalNames); j++)
        {
        if(m_logicalNames[j] == logical)
          {
          exists = true;
          break;
          }
        }
      if(!exists)
        {
        int sz = ArraySize(m_logicalNames);
        ArrayResize(m_logicalNames, sz + 1);
        ArrayResize(m_brokerNames, sz + 1);
        m_logicalNames[sz] = logical;
        m_brokerNames[sz] = brokerName;
        added++;
        if(verbose)
          Print("  Strip match: ", logical, " -> ", brokerName);
        }
      }
    }

  //--- Step 3: Fallback: exact major symbols (if they exist in terminal)
  if(added == 0)
    {
    string majors[] = {"EURUSD", "GBPUSD", "USDJPY", "AUDUSD", "USDCAD", "NZDUSD",
                       "XAUUSD", "XAGUSD", "US30", "SP500", "NAS100", "BTCUSD", "ETHUSD"
                      };
    for(int i = 0; i < ArraySize(majors); i++)
      {
      //--- Attempt to select the symbol (makes it available in Market Watch)
      if(SymbolSelect(majors[i], true))
        {
        int sz = ArraySize(m_logicalNames);
        ArrayResize(m_logicalNames, sz + 1);
        ArrayResize(m_brokerNames, sz + 1);
        m_logicalNames[sz] = majors[i];
        m_brokerNames[sz] = majors[i];
        added++;
        if(verbose)
          Print("  Fallback: ", majors[i], " -> ", majors[i]);
        }
      }
    }

  Print("MappingStorage: Generated ", added, " heuristic mappings.");
  return(added > 0);
  }

Each symbol is evaluated once during generation, which keeps the process deterministic.

Primary Heuristic: Description-Based Mapping

This is the strongest heuristic in the storage layer because broker descriptions are often more stable than symbol names.

//+------------------------------------------------------------------+
//| Attempt to deduce logical name from SYMBOL_DESCRIPTION           |
//+------------------------------------------------------------------+
string CMappingStorage::LogicalFromDescription(const string brokerName)
  {
  //--- Retrieve the human‑readable description of the symbol
  string desc = SymbolInfoString(brokerName, SYMBOL_DESCRIPTION);
  if(desc == "")
    return("");

  //--- Convert to lowercase for case‑insensitive keyword matching
  string low = desc;
  StringToLower(low);

  //--- Map common keywords in the description to canonical logical names
  if(StringFind(low, "gold") != -1)
    return("XAUUSD");
  if(StringFind(low, "silver") != -1)
    return("XAGUSD");
  if(StringFind(low, "dow") != -1 || StringFind(low, "us30") != -1)
    return("US30");
  if(StringFind(low, "sp500") != -1 || StringFind(low, "s&p") != -1)
    return("SP500");
  if(StringFind(low, "nasdaq") != -1 || StringFind(low, "nas100") != -1)
    return("NAS100");
  if(StringFind(low, "dax") != -1 || StringFind(low, "ger40") != -1)
    return("GER40");
  if(StringFind(low, "ftse") != -1 || StringFind(low, "uk100") != -1)
    return("UK100");
  if(StringFind(low, "brent") != -1)
    return("UKOIL");
  if(StringFind(low, "wti") != -1)
    return("USOIL");
  if(StringFind(low, "bitcoin") != -1)
    return("BTCUSD");
  if(StringFind(low, "ethereum") != -1)
    return("ETHUSD");
  if(StringFind(low, "euro") != -1 && StringFind(low, "usd") != -1)
    return("EURUSD");
  if(StringFind(low, "pound") != -1 && StringFind(low, "usd") != -1)
    return("GBPUSD");
  if(StringFind(low, "yen") != -1 && StringFind(low, "usd") != -1)
    return("USDJPY");
  if(StringFind(low, "australian") != -1 && StringFind(low, "usd") != -1)
    return("AUDUSD");
  if(StringFind(low, "canadian") != -1 && StringFind(low, "usd") != -1)
    return("USDCAD");
  if(StringFind(low, "new zealand") != -1 && StringFind(low, "usd") != -1)
    return("NZDUSD");

  //--- No match found
  return("");
  }

If the description contains keywords such as gold, silver, nasdaq, dow, or sp500, the system can map them to XAUUSD, XAGUSD, NAS100, US30, or SP500.

This often produces reliable mappings without requiring structural normalization.

Structural Normalization Layer

When descriptions are not enough, the system falls back to structural normalization.

A broker may add prefixes like OANDA_, FXCM_, ICM_, or MT5_, or suffixes like .fx, .pro, .cfd, .m, or _USD.

//+------------------------------------------------------------------+
//| Strip common prefixes from broker symbol names                   |
//+------------------------------------------------------------------+
string CMappingStorage::StripPrefix(const string name)
  {
  //--- List of common prefixes used by various brokers
  string prefixes[15] = {"OANDA_", "FXCM_", "XM_", "ICM_", "MT5_",
                         "CFD_", "PRO_", "ECN_", "LMAX_", "DUKA_",
                         "AUS_", "NZ_", "SG_", "HK_", "JP_"
                        };
  for(int i = 0; i < 15; i++)
    {
    if(StringFind(name, prefixes[i]) == 0)
      return(StringSubstr(name, StringLen(prefixes[i])));
    }
  return(name);
  }

//+------------------------------------------------------------------+
//| Strip common suffixes from broker symbol names                   |
//+------------------------------------------------------------------+
string CMappingStorage::StripSuffix(const string name)
  {
  //--- List of common suffixes (including currency labels and market identifiers)
  string suffixes[25] = {".m", ".fx", ".pro", ".cash", ".c", ".lmax",
                         "_USD", "_EUR", "_GBP", ".uk", ".us", ".cfd",
                         ".ecn", ".f", "_f", ".spot", ".nq", ".cm",
                         ".nymex", ".b", ".de", ".au", ".ca", ".jpy",
                         ".chf"
                        };
  for(int i = 0; i < 25; i++)
    {
    int pos = StringFind(name, suffixes[i]);
    //--- Only strip if the suffix is at the very end of the string
    if(pos != -1 && pos == StringLen(name) - StringLen(suffixes[i]))
      return(StringSubstr(name, 0, pos));
    }
  return(name);
  }

The normalization layer removes common broker-specific prefixes and suffixes before evaluating the remaining symbol name.

Mapping Generation Pipeline

The pipeline defines the full resolution strategy. Each symbol passes through a structured hierarchy:

  1. Try description-based mapping.
  2. If that fails, apply prefix stripping.
  3. Then apply suffix stripping.
  4. If a valid logical symbol remains, store it.

This ensures that every available symbol is processed through at least one deterministic mapping path, reducing the likelihood of unmapped instruments.

Persistence Layer (Save Operation)

//+------------------------------------------------------------------+
//| Save current mappings to a CSV file                              |
//+------------------------------------------------------------------+
bool CMappingStorage::SaveToFile(const string filename)
  {
  if(TotalMappings() == 0)
    return(false);

  //--- Open CSV file for writing (will overwrite existing)
  int handle = FileOpen(filename, FILE_WRITE | FILE_TXT | FILE_CSV, ",");
  if(handle == INVALID_HANDLE)
    {
    Print("MappingStorage: Cannot save to ", filename);
    return(false);
    }

  //--- Write the header line
  FileWrite(handle, "logicalName", "brokerName");

  //--- Write each mapping pair
  for(int i = 0; i < TotalMappings(); i++)
    FileWrite(handle, m_logicalNames[i], m_brokerNames[i]);

  FileClose(handle);
  Print("MappingStorage: Saved ", TotalMappings(), " mappings to ", filename);
  return(true);
  }


Once mappings are discovered, they are saved to CSV. That makes the system faster on the next run and avoids rebuilding the same knowledge repeatedly.

Unknown Symbol Logging

//+------------------------------------------------------------------+
//| Records an unresolved logical name for later review              |
//+------------------------------------------------------------------+
void CMappingStorage::LogUnknown(const string logical)
  {
  //--- Append the logical name to the unknown log list
  int sz = ArraySize(m_unknownLog);
  ArrayResize(m_unknownLog, sz + 1);
  m_unknownLog[sz] = logical;
  }

//+------------------------------------------------------------------+
//| Write the unknown-log list to a CSV file                         |
//+------------------------------------------------------------------+
void CMappingStorage::SaveUnknownLog(const string filename = "unknown_symbols.csv")
  {
  if(ArraySize(m_unknownLog) == 0)
    return;

  int handle = FileOpen(filename, FILE_WRITE | FILE_TXT | FILE_CSV, ",");
  if(handle == INVALID_HANDLE)
    return;

  //--- Write a single‑column header and then each logged name
  FileWrite(handle, "UnknownLogicalSymbol");
  for(int i = 0; i < ArraySize(m_unknownLog); i++)
    FileWrite(handle, m_unknownLog[i]);

  FileClose(handle);
  Print("Unknown symbols logged to ", filename);
  }

Unresolved symbols are stored separately so they can be reviewed later. This provides visibility into unresolved requests and helps refine future mapping rules.


Building the Symbol Resolution Engine

From the trading strategy's perspective, the framework exposes a single integration point: resolving a logical instrument name into a broker-specific symbol. The strategy interacts only with logical names such as EURUSD, XAUUSD, or US30, while the resolver either returns a validated broker symbol or an empty result when no valid mapping exists. The remainder of this section explains how that contract is fulfilled efficiently at runtime.

The mapping storage layer gives us a repository of discovered broker symbols, but it does not solve the runtime problem. Trading systems still need a mechanism that can translate a logical instrument name into a broker-specific symbol quickly and reliably while the Expert Advisor is executing.

This responsibility belongs to the symbol resolution engine.

The resolver acts as the central decision layer between trading logic and broker-specific naming conventions. When a strategy requests EURUSD, XAUUSD, or US30, the resolver selects the corresponding broker symbol.

To keep execution efficient, the resolver follows a layered lookup strategy:

  • Hash-based lookup for known mappings
  • Validation of stored broker symbols
  • Lazy discovery for previously unseen symbols

Under normal conditions, symbol resolution should complete without scanning the terminal symbol list. Discovery is treated as an exception path rather than a primary operating mode.

Internal Architecture

//+------------------------------------------------------------------+
//| SymbolMapper class declaration                                   |
//+------------------------------------------------------------------+
class SymbolMapper
  {
private:
  //--- Parallel arrays to store logical → broker pairs
  string            m_logicalNames[];
  string            m_brokerNames[];

  //--- Hash table for fast lookup of logical names (open addressing)
  int               m_hashTable[];
  int               m_hashSize;          
  int               m_count;             

  bool              m_allowLazySearch;   
  CMappingStorage*  m_storage;           

  //--- Alias tables for common symbol variants (e.g., "GOLD" → "XAUUSD")
  string            m_aliasLogical[];
  string            m_aliasBroker[];

  //--- Private helper functions
  void              InitAliases();
  bool              IsBrokerNameValidForLogical(const string logical, const string broker);
  int               HashName(const string &name) const;
  bool              InsertPair(const string logical, const string broker, bool validate = true);
  void              LoadDefaults();
  string            LazyResolve(const string logical);
  bool              IsSelectable(const string brokerName);

public:
  SymbolMapper();
  ~SymbolMapper() {}

  //--- Initialisation methods
  bool              Init(string configFile = NULL);
  bool              Initialize(CMappingStorage* storage);
  void              EnableLazySearch(bool enable) { m_allowLazySearch = enable; }

  //--- Core resolution functions
  string            Resolve(const string logicalName);
  string            ReverseResolve(const string brokerName);
  bool              Exists(const string logicalName);
  bool              AddCustomMapping(const string logicalName, const string brokerName);

  //--- Persistence
  void              SaveMappingsToStorage();
  void              Reset();

private:
  int               FindByLogical(const string &logical) const;
  };

The engine maintains a compact in-memory registry of mappings indexed through a hash table. This avoids linear search during repeated resolution calls and keeps execution cost stable even as mappings grow.

Initialization Defaults

//+------------------------------------------------------------------+
//| Loads a set of default mappings (exact match)                    |
//+------------------------------------------------------------------+
void SymbolMapper::LoadDefaults()
  {
  //--- These are the most common symbols; they will be overwritten
  //--- by any mappings loaded from file or storage.
  InsertPair("EURUSD", "EURUSD", false);
  InsertPair("GBPUSD", "GBPUSD", false);
  InsertPair("USDJPY", "USDJPY", false);
  InsertPair("AUDUSD", "AUDUSD", false);
  InsertPair("USDCAD", "USDCAD", false);
  InsertPair("NZDUSD", "NZDUSD", false);
  InsertPair("XAUUSD", "XAUUSD", false);
  InsertPair("XAGUSD", "XAGUSD", false);
  InsertPair("US30",   "US30",   false);
  InsertPair("SP500",  "SP500",  false);
  InsertPair("NAS100", "NAS100", false);
  InsertPair("BTCUSD", "BTCUSD", false);
  InsertPair("ETHUSD", "ETHUSD", false);
  InsertPair("GER40",  "GER40",  false);
  InsertPair("UK100",  "UK100",  false);
  InsertPair("USOIL",  "USOIL",  false);
  InsertPair("UKOIL",  "UKOIL",  false);
  }

Although most mappings will eventually come from storage or discovery, the resolver begins with a small set of predefined instruments. These defaults provide immediate usability and ensure that common symbols remain available even before a mapping file has been generated.

Storage Integration Layer

During initialization, the resolver imports mappings from the storage layer and rebuilds its runtime lookup structures.

//+------------------------------------------------------------------+
//| Initializes the mapper from a CMappingStorage object             |
//+------------------------------------------------------------------+
bool SymbolMapper::Initialize(CMappingStorage* storage)
  {
  if(storage == NULL)
    return(false);

  m_storage = storage;
  Reset();
  LoadDefaults();                        

  int validAdded = 0;
  //--- Iterate over all mappings in the storage and add selectable ones
  for(int i = 0; i < storage.TotalMappings(); i++)
    {
    string logical, broker;
    if(storage.GetMapping(i, logical, broker))
      {
      if(IsBrokerNameValidForLogical(logical, broker))
        {
        //--- Only add if the broker symbol is actually available
        if(IsSelectable(broker))
          {
          InsertPair(logical, broker);
          validAdded++;
          }
        else
          {
          Print("SymbolMapper: Skipping non-selectable ", logical, " -> ", broker);
          }
        }
      else
        {
        Print("SymbolMapper: Skipping invalid mapping ", logical, " -> ", broker);
        }
      }
    }

  Print("SymbolMapper: Initialized with ", m_count, " mappings (", validAdded, " from storage).");
  return(true);
  }
This separation keeps persistence concerns inside the storage layer while allowing the resolver to operate entirely from memory during normal execution.

Resolution Workflow

When a logical symbol is requested, the resolver attempts to satisfy the request using the fastest available path.

//+------------------------------------------------------------------+
//| Resolves a logical name to a broker symbol name                  |
//+------------------------------------------------------------------+
string SymbolMapper::Resolve(const string logicalName)
  {
  if(logicalName == "")
    return("");

  //--- Try cache first
  int idx = FindByLogical(logicalName);
  if(idx >= 0)
    {
    string broker = m_brokerNames[idx];
    if(IsSelectable(broker))
      return(broker);

    //--- If cached broker is not selectable, try lazy search (if allowed)
    if(m_allowLazySearch && m_storage != NULL)
      {
      string alt = LazyResolve(logicalName);
      if(alt != "")
        return(alt);
      }
    return(broker);
    }

  //--- Not in cache: perform lazy resolution if enabled
  if(m_allowLazySearch && m_storage != NULL)
    {
    string result = LazyResolve(logicalName);
    if(result != "")
      return(result);
    }

  //--- If all fails, log and return empty
  Print("SymbolMapper: Cannot resolve ", logicalName);
  return("");
  }

The process follows a simple hierarchy:

  • Search the hash table for an existing mapping.
  • Verify that the stored broker symbol is selectable.
  • Return it immediately when available.
  • Otherwise, perform lazy discovery if it is enabled.
  • If lazy discovery succeeds, return the discovered symbol; otherwise, fall back to the stored mapping.

This approach keeps symbol resolution efficient while allowing the resolver to recover from unavailable broker symbols when possible.

    Lazy Discovery Engine

    When a mapping is missing, the resolver scans the terminal symbol list, compares candidate names, and chooses the best selectable match. If it succeeds, the new mapping is inserted immediately so the discovery cost is paid only once.
    //+------------------------------------------------------------------+
    //| Performs lazy resolution when a logical name is not in cache    |
    //+------------------------------------------------------------------+
    string SymbolMapper::LazyResolve(const string logical)
      {
      if(m_storage == NULL)
        return("");
    
      //--- Step 1: Check alias table (e.g., "GOLD" → "XAUUSD")
      for(int i = 0; i < ArraySize(m_aliasLogical); i++)
        {
        if(m_aliasLogical[i] == logical)
          {
          //--- If the alias exists in terminal, add it to the cache
          if(SymbolSelect(m_aliasBroker[i], true))
            {
            InsertPair(logical, m_aliasBroker[i]);
            Print("SymbolMapper: Alias match ", logical, " -> ", m_aliasBroker[i]);
            return(m_aliasBroker[i]);
            }
          }
        }
    
      //--- Step 2: Substring search across all symbols in Market Watch
      int total = SymbolsTotal(false);
      string bestMatch = "";
      int bestScore = 0;
      string lowerLog = logical;
      StringToLower(lowerLog);
    
      for(int i = 0; i < total; i++)
        {
        string name = SymbolName(i, false);
        string lowerName = name;
        StringToLower(lowerName);
        if(StringFind(lowerName, lowerLog) != -1)
          {
          int score = 100 - StringLen(name);   
          if(score > bestScore)
            {
            bestScore = score;
            bestMatch = name;
            }
          }
        }
    
      //--- If a match was found and selectable, add it to the cache
      if(bestMatch != "" && SymbolSelect(bestMatch, true))
        {
        InsertPair(logical, bestMatch);
        Print("SymbolMapper: Lazy search found ", logical, " -> ", bestMatch);
        return(bestMatch);
        }
    
      //--- No resolution found: log the unknown logical name for later review
      if(m_storage != NULL)
        m_storage.LogUnknown(logical);
    
      return("");
      }

    Lazy discovery exists to handle symbols that were not previously learned by the framework. Rather than failing immediately, the resolver performs a controlled scan of the terminal symbol list and attempts to identify the most appropriate candidate.

    The scoring mechanism favors shorter matches because broker-specific decorations often increase symbol length. Once a suitable symbol is identified and selected successfully, the mapping is inserted into the resolver and becomes available for future requests.

    Validation Layer

    Before a mapping is accepted, the resolver performs a lightweight consistency check between the logical symbol and the broker symbol.
    //+------------------------------------------------------------------+
    //| Validates whether a broker name is acceptable for a logical      |
    //+------------------------------------------------------------------+
    bool SymbolMapper::IsBrokerNameValidForLogical(const string logical, const string broker)
      {
      if(logical == "" || broker == "")
        return(false);
    
      //--- Exact match is always valid
      if(logical == broker)
        return(true);
    
      //--- Case‑insensitive substring match (e.g., "EURUSD" inside "EURUSD.ecn")
      string lowerLogical = logical;
      string lowerBroker = broker;
      StringToLower(lowerLogical);
      StringToLower(lowerBroker);
      if(StringFind(lowerBroker, lowerLogical) != -1)
        return(true);
    
      //--- Check against explicit aliases
      for(int i = 0; i < ArraySize(m_aliasLogical); i++)
        {
        if(m_aliasLogical[i] == logical && m_aliasBroker[i] == broker)
          return(true);
        }
      return(false);
      }

    The objective is to reject clearly unrelated mappings without enforcing strict naming rules. This protects the runtime mapping table from accidental pollution while still allowing broker-specific naming variations.

    Hash Lookup Engine

    The hash table is the primary acceleration mechanism used by the resolver. Rather than scanning the mapping array sequentially, the resolver computes a hash value for the logical symbol and jumps directly to the most likely storage location. The mapping arrays remain the underlying source of symbol relationships, while the hash table serves only as an index.
    //+------------------------------------------------------------------+
    //| Searches for a logical name in the hash table                    |
    //+------------------------------------------------------------------+
    int SymbolMapper::FindByLogical(const string &logical) const
      {
      if(logical == "" || m_hashSize == 0)
        return(-1);
    
      int idx = HashName(logical);
      int startIdx = idx;
    
      //--- Linear probing until we find the key or hit an empty slot
      while(m_hashTable[idx] != -1)
        {
        int arrayIndex = m_hashTable[idx];
        if(m_logicalNames[arrayIndex] == logical)
          return(arrayIndex);
    
        idx = (idx + 1) & (m_hashSize - 1);
        if(idx == startIdx)   
          break;
        }
      return(-1);
      }

    Collisions are handled through open addressing, allowing lookup performance to remain predictable even as the mapping dataset grows.

    Persistence Synchronization

    Runtime-discovered mappings should be written back to storage so the next session starts with better data. The resolver itself never writes files directly. It only synchronizes runtime state back into CMappingStorage, which remains the single owner of persistence responsibilities. This separation keeps resolution logic independent of storage implementation and preserves clear ownership boundaries.

    //+------------------------------------------------------------------+
    //| Writes all current mappings back to the attached storage         |
    //+------------------------------------------------------------------+
    void SymbolMapper::SaveMappingsToStorage()
      {
      if(m_storage == NULL)
        return;
      //--- Replace all mappings in the storage with the current ones
      m_storage.Reset();
      for(int i = 0; i < m_count; i++)
        m_storage.AddMapping(m_logicalNames[i], m_brokerNames[i]);
      }

    If these mappings are discarded at shutdown, the resolver would be forced to rediscover the same symbols repeatedly.

    To avoid that overhead, the runtime mapping table is synchronized back into the storage layer before persistence occurs. This transforms symbol resolution into a self-improving process where each successful discovery benefits future sessions.

    Resolution Engine Summary

    The resolver now provides fast runtime translation between logical and broker-specific symbols. Through hash-based lookup, validation, and controlled discovery, trading logic can remain independent of broker naming conventions.


    Adding the Resolution Cache

    The resolver can already find broker symbols, but many Expert Advisors request the same logical names repeatedly. Without a cache, each request would go through the full lookup path again, even when the answer has already been discovered. The cache removes that repetition by storing successful resolutions and returning them immediately on future calls.

    Cache Structure

    The cache uses two parallel arrays. One stores the logical symbol name, and the other stores the resolved broker symbol.

    //+------------------------------------------------------------------+
    //| CResolutionCache class declaration                                |
    //+------------------------------------------------------------------+
    class CResolutionCache
      {
    private:
      SymbolMapper*     m_mapper;
      string            m_cacheKeys[];
      string            m_cacheVals[];

    This keeps the implementation simple and predictable, which is enough for the small symbol sets used by most Expert Advisors.

    Cache-First Resolution Flow

    Every request begins with a cache lookup. If the logical symbol has already been resolved, the stored broker symbol is returned immediately. Only when the cache misses does the request move to the resolver.

    That keeps the expensive parts of the process, such as mapping lookup and lazy discovery, out of the normal hot path.

    Integration with Resolution Engine

    The cache does not resolve symbols on its own. It delegates unknown requests to the mapper, then stores the result if the resolution succeeds. In that sense, the cache acts as a fast front layer around the resolver rather than a separate mapping system.

    Cache Resolution Implementation

    //+------------------------------------------------------------------+
    //| Resolves a logical name, caching the result                       |
    //+------------------------------------------------------------------+
    string CResolutionCache::Resolve(const string logicalName)
      {
      if(logicalName == "")
        return("");
    
      //--- Check cache first
      for(int i = 0; i < ArraySize(m_cacheKeys); i++)
        {
        if(m_cacheKeys[i] == logicalName)
          return(m_cacheVals[i]);
        }
    
      if(m_mapper == NULL)
        {
        Print("ResolutionCache: No mapper set.");
        return("");
        }
    
      string resolved = m_mapper.Resolve(logicalName);
      if(resolved != "")
        {
        int sz = ArraySize(m_cacheKeys);
        ArrayResize(m_cacheKeys, sz + 1);
        ArrayResize(m_cacheVals, sz + 1);
        m_cacheKeys[sz] = logicalName;
        m_cacheVals[sz] = resolved;
        }
      return(resolved);
      }

    The sequence is straightforward: check the cache, call the mapper on a miss, and store the result when the lookup succeeds. That way, each logical symbol is resolved only once during normal runtime use.

    Cache Maintenance

    //+------------------------------------------------------------------+
    //| Clears all cached entries                                        |
    //+------------------------------------------------------------------+
    void CResolutionCache::ClearCache()
      {
      ArrayFree(m_cacheKeys);
      ArrayFree(m_cacheVals);
      }

    Cache invalidation is handled with a full reset. That is enough here because mappings are expected to remain stable unless the resolver is reinitialized or the structure of the mapping data changes.

    Constructor and Mapper Binding

    //+------------------------------------------------------------------+
    //| Constructor                                                      |
    //+------------------------------------------------------------------+
    CResolutionCache::CResolutionCache()
      {
      m_mapper = NULL;
      }
    
    //+------------------------------------------------------------------+
    //| Attaches a SymbolMapper instance                                  |
    //+------------------------------------------------------------------+
    void CResolutionCache::SetMapper(SymbolMapper* mapper)
      {
      m_mapper = mapper;
      }

    The mapper is attached explicitly so the cache remains independent of initialization order. That avoids null resolution paths and keeps the dependency chain clear.

    Cache Layer Summary

    The cache eliminates repeated resolution work by storing successful lookups locally. As a result, most requests can be served directly from memory with minimal overhead.


    Building the Host Verification Expert Advisor

    At this point, the framework can discover broker symbols, persist mappings, resolve logical names, and accelerate repeated lookups through caching. The remaining question is whether those mappings are actually usable inside a live terminal.

    A mapping that looks correct in memory may still fail in practice. The broker symbol might not be selectable, market data may be unavailable, or the instrument could be inactive. Before integrating the framework into a trading strategy, we therefore need a way to verify that resolved symbols remain operational under real terminal conditions.

    To perform that validation, we build a dedicated verification Expert Advisor.

    Unlike a trading EA, this module never places orders. Its sole responsibility is to test the complete symbol resolution pipeline and confirm that the resulting broker symbols are available for use.

    System Initialization Pipeline

    The verification EA begins by constructing the same components used by the production framework.

    //+------------------------------------------------------------------+
    //| Global objects and state variables                               |
    //+------------------------------------------------------------------+
    SymbolMapper*    g_SymbolMapper = NULL;   // Main symbol resolver
    CMappingStorage* g_Storage      = NULL;   // Persistent mapping storage (CSV)
    CResolutionCache*g_Cache        = NULL;   // Cache for resolved symbols (speeds up repeated lookups)
    
    string           g_ResolvedSymbols[];     // Last resolved broker names for each test symbol
    datetime         g_LastCheckTime = 0;     // Timestamp of the last verification cycle
    bool             g_MappingReady  = false; // Flag indicating the system is fully initialised

    The initialization order is important because each layer depends on the one beneath it.

    • Create and initialize the storage layer.
    • Initialize the symbol resolver using the stored mappings.
    • Attach the cache to the resolver.
    • Start the verification cycle.

    By following the same dependency chain used in production, the verification environment closely mirrors real-world execution.

    Mapping Bootstrap (Load or Generate)

    Before verification can begin, the EA must obtain a valid mapping dataset. The preferred approach is to load a previously generated mapping file.

    //--- Load or generate mapping
    bool loaded = g_Storage.LoadFromFile(InpMappingFileName);
    if(!loaded)
      {
      Print("INFO: No mapping file. Generating from symbol list...");
      if(!g_Storage.GenerateAll(InpVerbose))
        Print("WARNING: Could not generate heuristic mappings.");
      else
        g_Storage.SaveToFile(InpMappingFileName);
      }
    else
      {
      Print("INFO: Loaded ", g_Storage.TotalMappings(), " mappings from ", InpMappingFileName);
      }

    If no mapping file exists, the EA automatically generates a new dataset from the broker's symbol list and immediately persists the results.

    This allows the verification process to remain fully self-contained and eliminates the need for manual setup.

    Mapper and Cache Binding

    Once the mapping dataset is available, the resolver and cache can be connected.

    //--- Initialize mapper
    if(!g_SymbolMapper.Initialize(g_Storage))
      {
      Print("ERROR: SymbolMapper init failed.");
      delete g_Storage;
      delete g_SymbolMapper;
      delete g_Cache;
      return(INIT_FAILED);
      }
    
    g_SymbolMapper.EnableLazySearch(true);
    g_Cache.SetMapper(g_SymbolMapper);

    After initialization, the mapper is connected to the storage layer and enabled for lazy discovery. The cache is then attached as a front-layer resolver.

    This establishes the runtime resolution pipeline:

    Cache

      ↓

    SymbolMapper

      ↓

    MappingStorage

    All symbol requests flow through the cache first. If a symbol is already known, the result is returned immediately. Otherwise, the request is delegated to the resolver, which may consult stored mappings or perform lazy discovery when necessary.

    Because the verification EA interacts only with the cache, the underlying implementation remains fully encapsulated.

    By interacting exclusively with the cache layer, the verification EA exercises the same execution path that would be used by a production Expert Advisor. This ensures that testing reflects actual runtime behavior rather than isolated component behavior.

    Verification Cycle Design

    Verification is performed continuously using a timer-driven execution model. During each cycle, the EA processes a predefined list of logical symbols and evaluates whether they can be resolved and used successfully.

    The objective is not simply to confirm that a mapping exists, but to verify that the resolved symbol remains operational within the terminal.

    Each cycle therefore validates:

    • Resolution correctness
    • Symbol availability
    • Market data accessibility

    Only symbols that pass all validation stages are considered usable.

    Runtime Verification Logic

    The core validation process is implemented inside the timer event handler.

    //+------------------------------------------------------------------+
    //| Timer function                                                   |
    //+------------------------------------------------------------------+
    void OnTimer()
      {
      //--- Do nothing if the system is not ready
      if(!g_MappingReady)
        return;
    
      //--- Re‑parse the test symbols in case the input changed (runtime update)
      string testSymbols[];
      int count = StringSplit(InpTestSymbols, ',', testSymbols);
      if(count == 0)
        return;
    
      //--- Reset the resolved symbol array
      ArrayResize(g_ResolvedSymbols, count);
      for(int i = 0; i < count; i++)
        g_ResolvedSymbols[i] = "";
    
      datetime now = TimeCurrent();
      Print("=== Verification Cycle at ", TimeToString(now), " ===");
    
      //--- Iterate over each symbol in the test list
      for(int i = 0; i < count; i++)
        {
        string genericSym = testSymbols[i];
        StringTrimLeft(genericSym);
        StringTrimRight(genericSym);
        if(genericSym == "")
          continue;
    
        //--- Resolve the logical name to a broker symbol using the cache (which uses the mapper)
        string resolved = g_Cache.Resolve(genericSym);
        if(resolved == "")
          {
          Print("   [FAIL] ", genericSym, " -> UNRESOLVED");
          g_ResolvedSymbols[i] = "UNRESOLVED";
          continue;
          }
    
        //--- Ensure the resolved symbol is actually available in Market Watch
        if(!SymbolSelect(resolved, true))
          {
          Print("   [FAIL] ", genericSym, " -> ", resolved, " (not selectable)");
          g_ResolvedSymbols[i] = resolved + " (INVALID)";
          continue;
          }
    
        //--- Retrieve bid/ask quotes to confirm the symbol is live
        double bid = SymbolInfoDouble(resolved, SYMBOL_BID);
        double ask = SymbolInfoDouble(resolved, SYMBOL_ASK);
        if(bid == 0.0 || ask == 0.0)
          Print("   [WARN] ", genericSym, " -> ", resolved, " (bid/ask=0)");
    
        //--- All checks passed: log the successful resolution with current prices
        Print("   [OK]   ", genericSym, " -> ", resolved,
              "  bid=", DoubleToString(bid, _Digits),
              "  ask=", DoubleToString(ask, _Digits));
        g_ResolvedSymbols[i] = resolved;
        }
    
      //--- Update the last check timestamp and finish the cycle
      g_LastCheckTime = now;
      Print("=== Cycle complete ===\n");
      }

    For every logical symbol, the EA applies a staged validation process:

    Logical Symbol

           ↓

    Cache Resolution

           ↓

    Symbol Selection

           ↓

    Market Data Validation

           ↓

    Verification Result

    The process stops immediately when a stage fails, making it easy to identify exactly where a resolution problem occurs.

      Market-Level Validation

      The final verification stage focuses on terminal usability rather than symbol discovery.

      A symbol may resolve correctly and still be unsuitable for trading. For example, the broker might expose the symbol while withholding market data, disabling trading access, or preventing the instrument from being selected.

      To detect these conditions, the EA validates:

      • Successful SymbolSelect()
      • Availability of bid pricing
      • Availability of ask pricing
      • Consistent accessibility across repeated verification cycles

      This protects the framework from accepting mappings that are technically correct but operationally unusable.

      Failure Recovery

      Not every symbol request can be resolved successfully. Some instruments may be unavailable on a particular broker, temporarily disabled, or simply unsupported by the current mapping rules.

      An important design goal of the framework is graceful failure. When a symbol cannot be resolved, the system returns a controlled failure result, records the unresolved request, and continues operating without affecting unrelated symbols.

      This prevents isolated mapping problems from propagating into wider system failures and allows the Expert Advisor to remain operational even when partial mapping coverage exists.

      Mapping Persistence at Shutdown

      Verification can discover new information during runtime, particularly when lazy discovery identifies previously unknown symbols.

      To preserve those discoveries, the EA synchronizes runtime mappings back into persistent storage before shutdown.

      //+------------------------------------------------------------------+
      //| Expert deinitialization function                                 |
      //+------------------------------------------------------------------+
      void OnDeinit(const int reason)
        {
        //--- Stop the timer first
        EventKillTimer();
      
        //--- Save any mappings that were discovered via lazy resolution
        if(g_SymbolMapper != NULL && g_Storage != NULL)
          {
          g_SymbolMapper.SaveMappingsToStorage();   // Transfer mapper's cache back to storage
          if(g_Storage.TotalMappings() > 0)
            g_Storage.SaveToFile(InpMappingFileName);
          g_Storage.SaveUnknownLog();               // Save unresolved symbols for review
          }
      
        //--- Clean up allocated objects
        delete g_SymbolMapper;
        delete g_Storage;
        delete g_Cache;
        ArrayFree(g_ResolvedSymbols);
        Print("HostVerification deinitialized.");
        }

      The shutdown phase persists:

      • Newly discovered mappings
      • Runtime updates to existing mappings
      • Unresolved symbol records

      This transforms verification into a learning process. Each execution cycle contributes additional knowledge to the framework, reducing future discovery overhead and improving symbol coverage over time.

      Verification Layer Summary

      The verification EA confirms that resolved symbols are not only discoverable but also usable inside the terminal. This provides confidence that mappings remain valid under live broker conditions.


      Benchmarking the Resolution Layer

      After building and validating the complete resolution framework, the next question is whether the design is efficient enough for real trading workloads.

      A symbol resolver may be functionally correct while still introducing unnecessary overhead. In a multi-symbol Expert Advisor, resolution requests can occur repeatedly inside tick handlers, timer events, portfolio scanners, and risk management routines. Even small inefficiencies become noticeable when executed thousands of times during a trading session.

      For that reason, we introduce a dedicated benchmark module that isolates the resolution process and measures its execution cost under controlled conditions.

      The objective is not simply to obtain timing numbers, but to verify that the architectural decisions made earlier—hash-based lookup, persistent mappings, lazy discovery, and caching—actually deliver the expected runtime behavior.

      Benchmark Design

      The benchmark focuses on three execution paths:

      1. Cached resolution
      2. Stored mapping lookup
      3. Lazy discovery fallback

      Under normal operating conditions, the vast majority of requests should be resolved through the first path. The benchmark allows us to verify whether that assumption holds true.

      The expected hierarchy is:

      Cache Hit

         ↓

      Stored Mapping Lookup

         ↓

      Lazy Discovery

      As long as the average execution time remains close to cache-hit latency, the framework can be considered efficient for production use.

      Benchmark Runtime Objects

      The benchmark maintains a pool of logical symbols together with a collection buffer used for latency measurements.

      //+------------------------------------------------------------------+
      //| Global variables                                                 |
      //+------------------------------------------------------------------+
      SymbolMapper g_mapper;               // The symbol resolver being benchmarked
      string       g_pool[];               // Pool of logical names to resolve
      int          g_index = 0;            // Current sample count
      ulong        g_samples[];            // Array to store measured times (microseconds)
      bool         g_ready = false;        // Flag indicating benchmark is ready

      The symbol pool contains a representative mix of forex pairs, metals, indices, and other commonly traded instruments. Rather than measuring a single symbol repeatedly, the benchmark rotates through multiple instruments to better simulate real-world EA behavior.

      This approach helps exercise both hot and cold resolution paths and provides a more realistic performance profile.

      Measuring Resolution Latency

      Each benchmark iteration measures the time required to complete a single symbol resolution request.

      The measurement sequence is intentionally simple:

      Start Timer

           ↓

      Resolve Symbol

           ↓

      Stop Timer

           ↓

      Store Duration

      Execution time is captured using GetMicrosecondCount(), which provides microsecond-level precision suitable for comparing fast in-memory operations.

      The resulting measurements include:

      • Cache-hit latency
      • Mapping lookup latency
      • Lazy-discovery latency

      By collecting a sufficiently large number of samples, we can evaluate the overall behavior of the framework rather than focusing on isolated measurements.

      Sampling Loop

      The benchmark executes continuously using a timer-driven event loop.

      //+------------------------------------------------------------------+
      //| Timer function: performs a single resolution and records time    |
      //+------------------------------------------------------------------+
      void OnTimer()
        {
        //--- Stop if benchmark is complete or not ready
        if(!g_ready || g_index >= SamplesToTake)
          return;
      
        //--- Pick a random logical name from the pool
        string logical = g_pool[MathRand() % ArraySize(g_pool)];
      
        //--- Measure the time taken to resolve it
        ulong start = GetMicrosecondCount();
        string resolved = g_mapper.Resolve(logical);
        ulong elapsed = GetMicrosecondCount() - start;
      
        //--- Only record successful resolutions
        if(resolved != "")
          {
          g_samples[g_index] = elapsed;
          g_index++;
          }
      
        //--- Print progress every 10,000 samples
        if(g_index % 10000 == 0 && g_index > 0)
          Print("Progress: ", g_index, " samples");
        }

      During each timer event, a logical symbol is selected from the pool and passed through the resolver. Only successful resolutions are recorded, ensuring that timing statistics reflect valid execution paths rather than error handling behavior.

      Over time, this produces a dataset that represents the actual cost of symbol resolution across repeated runtime requests.

      Statistical Analysis

      Once sampling is complete, the benchmark calculates a set of summary statistics.

      //+------------------------------------------------------------------+
      //| Expert deinitialization function: compute and report statistics  |
      //+------------------------------------------------------------------+
      void OnDeinit(const int reason)
        {
        //--- Stop the timer
        EventKillTimer();
      
        //--- Ensure we have enough samples for a meaningful result
        if(g_index < 100)
          {
          Print("Insufficient samples: ", g_index);
          return;
          }
      
        //--- Compute min, max, and sum of recorded times
        ulong minV = ULONG_MAX, maxV = 0, sum = 0;
        for(int i = 0; i < g_index; i++)
          {
          ulong v = g_samples[i];
          minV = MathMin(minV, v);
          maxV = MathMax(maxV, v);
          sum += v;
          }
        double avg = (double)sum / g_index;
      
        //--- Output the benchmark results
        Print("========== SymbolMapper Benchmark ==========");
        Print("Samples: ", g_index);
        Print("Min time: ", minV, " µs");
        Print("Max time: ", maxV, " µs");
        Print("Average: ", DoubleToString(avg, 2), " µs");
        Print("============================================");
        }

      Three values are particularly useful:

      Metric Meaning
      Minimum Fastest observed execution path
      Maximum Slowest observed execution path
      Average Typical runtime behavior

      Together, these metrics provide a concise view of how the resolver behaves under sustained load.

      For larger benchmark datasets, percentile measurements such as the 95th percentile can provide additional insight into worst-case behavior while reducing the influence of isolated outliers. This can be useful when evaluating runtime consistency across different broker environments.

      Interpreting the Results

      Raw timing numbers are only useful when viewed in context.

      The minimum value typically represents a cache hit, where the symbol is returned immediately from memory.

      The maximum value often corresponds to lazy discovery, where the resolver must scan the terminal symbol list before determining the best candidate.

      The average value is the most important metric because it reflects the behavior that trading systems experience during normal operation.

      A healthy benchmark usually exhibits the following pattern:

      Average ≈ Minimum

      Maximum >> Average

      This indicates that most requests are being served from the fast path, while slower discovery operations occur only occasionally.

      If the average value begins drifting toward the maximum, it is usually a sign that too many requests are falling back to discovery and that mapping coverage should be improved.

      Benchmark Initialization

      Before sampling begins, the benchmark loads the resolver and prepares the symbol pool.

      //+------------------------------------------------------------------+
      //| Expert initialization function                                   |
      //+------------------------------------------------------------------+
      int OnInit()
        {
        //--- Initialise the mapper; try loading a CSV, otherwise use defaults
        if(!g_mapper.Init("mappings.csv"))
          g_mapper.Init();
      
        //--- Define the pool of logical symbols to test
        string defaultSymbols[] = {"EURUSD", "GBPUSD", "USDJPY", "AUDUSD", "USDCAD",
                                   "NZDUSD", "XAUUSD", "XAGUSD", "US30", "SP500", "NAS100"
                                  };
        ArrayResize(g_pool, ArraySize(defaultSymbols));
        ArrayCopy(g_pool, defaultSymbols);
      
        if(ArraySize(g_pool) == 0)
          return(INIT_FAILED);
      
        //--- Pre‑allocate the samples array
        ArrayResize(g_samples, SamplesToTake);
        g_ready = true;
        Print("Benchmark started. Samples to take: ", SamplesToTake);
      
        //--- Start a fast timer (1 ms) to run the benchmark
        EventSetMillisecondTimer(1);
        return(INIT_SUCCEEDED);
        }

      The benchmark starts by loading previously discovered mappings and preparing a representative set of instruments. A millisecond timer then drives the sampling process until the required number of observations has been collected.

      This allows the benchmark to run automatically and generate a repeatable performance profile for the entire resolution framework.

      Benchmark Layer Summary

      The benchmark module provides an objective way to evaluate the efficiency of the symbol resolution framework under sustained load.

      It measures how often requests use cached paths, stored mappings, or lazy discovery, instead of relying on assumptions. This allows us to verify that the architectural decisions introduced throughout the article translate into measurable runtime performance.

      At this point, the framework has been validated functionally and measured quantitatively. The next step is to verify the resolved symbols in a live terminal environment. This shows how logical instrument names are translated into broker-specific symbols during normal operation.


      Verifying Symbol Resolution in a Live Terminal

      With the implementation complete, the final step is to validate the framework under real runtime conditions. The objective is not to evaluate trading performance, but to confirm that logical instrument names can be translated into broker-specific symbols and remain fully usable inside the terminal.

      To perform this verification, the Host Verification EA was executed using a predefined set of logical symbols. Each symbol was processed through the complete resolution pipeline:

      Cache → Mapper → Storage

      For every symbol, the framework attempted to:

      • Resolve the logical instrument name.
      • Select the resulting broker symbol.
      • Verify that valid market data was available.
      • Report the outcome through the terminal journal.

      Verification Output

      === Verification Cycle ===
      [OK] EURUSD -> EURUSD
      [OK] GBPUSD -> GBPUSD
      [OK] XAUUSD -> GOLD
      [OK] US30 -> US30Cash
      [OK] SP500 -> US500Cash
      [OK] NAS100 -> US100Cash
      [OK] GER40 -> GER40Cash
      [OK] UK100 -> UK100Cash
      [OK] USOIL -> OILCash
      [OK] UKOIL -> BRENTCash
      === Cycle Complete ===

      Resolution Results

      Logical Symbol Resolved Symbol
      EURUSD EURUSD
      GBPUSD GBPUSD
      XAUUSD GOLD
      US30 US30Cash
      SP500 US500Cash
      NAS100 US100Cash
      GER40 GER40Cash
      UK100 UK100Cash
      USOIL OILCash
      UKOIL BRENTCash

      The results demonstrate that the framework successfully translated multiple logical symbols into the broker-specific names exposed by the terminal. While some instruments were available using their canonical names, several required alias resolution before they could be used.


      Key Lessons

      Several important observations emerged during implementation and testing:

      • First, symbol abstraction proved significantly more maintainable than hardcoded broker symbols. Trading logic interacted exclusively with logical instrument names while broker-specific identifiers remained isolated inside the resolution framework.
      • Second, hash-based lookup and caching kept runtime overhead low by ensuring that most requests were resolved through in-memory paths rather than discovery operations.
      • Third, persistence allowed successful discoveries to be reused across sessions, reducing startup costs and improving mapping coverage over time.
      • Finally, graceful failure prevented unresolved symbols from affecting unrelated operations, allowing the framework to remain functional even when partial symbol coverage existed.

      Taken together, these observations indicate that symbol resolution is best treated as a dedicated infrastructure service rather than a responsibility embedded directly inside trading strategies.


      Conclusion

      The result is a practical, production‑oriented symbol resolution layer for MT5 composed of: a persistent mapping store (CSV plus automatic generation from terminal symbols), a hash‑accelerated SymbolMapper with controlled normalization and validation rules, an in‑memory ResolutionCache for hot paths, and a HostVerification EA that exercises and certifies each mapping using SymbolSelect and live bid/ask checks. Benchmarks and logs complete the workflow: the benchmark demonstrates that the average resolve time is dominated by cache hits while lazy discovery remains an occasional, logged fallback.

      Concrete guarantees: when Resolve(logicalName) returns a non-empty broker symbol, the framework guarantees that SymbolSelect succeeded and that SYMBOL BID/SYMBOL ASK are available; unresolved requests are logged to an “unknown” CSV for manual review. Runtime discoveries are persisted at shutdown so subsequent runs avoid repeat discovery. Failure semantics are explicit: the resolver returns an empty string on failure (no exceptions), allowing calling EAs to decide fallback behavior while the resolution system records details for remediation.

      To validate integration, check the HostVerification logs and the mappings CSV, confirm benchmark metrics show Average ≈ Minimum (cache‑dominated), and verify that trading operations (SymbolSelect, price reads, and OrderSend) work on a target broker without hardcoded symbol edits. This transforms symbol resolution from ad‑hoc string work into a reusable infrastructure service with measurable outcomes and a simple integration contract for trading strategies.

      Attached files |
      Features of Custom Indicators Creation Features of Custom Indicators Creation
      Creation of Custom Indicators in the MetaTrader trading system has a number of features.
      Persistent Homology in MQL5: The Reduction Algorithm and the Persistence Diagram Persistent Homology in MQL5: The Reduction Algorithm and the Persistence Diagram
      We complete persistent homology for MQL5 by reducing the Vietoris–Rips boundary matrix to a persistence diagram. The article implements Z/2 column reduction (CTDAReduction), a diagram container with analytics (CTDADiagram), and a facade that runs the six-stage pipeline in one call (CTDA). Outputs are cross-checked against Ripser to numerical agreement, enabling reliable diagram-based metrics.
      Features of Experts Advisors Features of Experts Advisors
      Creation of expert advisors in the MetaTrader trading system has a number of features.
      Overcoming Accessibility Problems in MQL5 Trading Tools (Part VI): Neural Command Integration Overcoming Accessibility Problems in MQL5 Trading Tools (Part VI): Neural Command Integration
      This article demonstrates a working prototype integrating Brain-Computer Interface technology with MetaTrader 5, proving thought-based trading is feasible at the software level. A Python Flask server simulates neural command generation, communicating with an MQL5 Expert Advisor via JSON-over-HTTP. The complete pipeline—from signal generation to trade execution—is validated through WebRequest and CTrade. While BCI hardware remains clinically restricted, this simulation establishes a reference architecture for future accessibility options, enabling direct intention-based trading that expands how traders can interact with financial markets.