Interactive Supply and Demand Zone Manager in MQL5 (Part II): Event-Driven Architecture and Persistent Lifecycle Logging
Introduction
In Part I of this series, we introduced a dynamic supply and demand zone management framework capable of transforming ordinary chart rectangles into actively managed market structures. Rather than treating support and resistance zones as passive visual annotations, we developed a system capable of creating, maintaining, merging, invalidating, and restoring structural regions as market conditions evolved.
The primary objective of the initial implementation was to establish the concept of a stateful zone. Each zone was no longer treated as a simple graphical object, but as a structured entity containing attributes such as classification, status, origin, strength, and interaction history. Within this model, a support level could transition from a newly created state into a broken state, while a previously invalidated structure could be retained in a ghosted form rather than immediately removed. Under defined conditions, such a zone could later be reactivated and restored to active status. This established the foundation of lifecycle-aware support and resistance management.
However, a fundamental limitation remained. Although the framework was capable of responding to runtime events, it did not retain a persistent record of those transitions. Manual modifications, such as resizing a zone, existed only in volatile memory. Similarly, structural transitions such as breakouts or invalidations were correctly applied in real time, but no durable representation of these events was preserved. As a result, once the terminal session ended or the chart was refreshed, the full operational history was lost. The system retained only the latest state of each zone, without any trace of how that state had been reached.
In parallel, an additional architectural limitation emerged. User interactions were handled through synchronization routines that continuously scanned chart objects for changes. While functional, this polling-based approach required repeated evaluation of object state regardless of whether any interaction had occurred, introducing unnecessary computational overhead and architectural inefficiency.
A more robust event-handling foundation is required before adding higher-level capabilities such as zone analytics, historical effectiveness evaluation, structural durability modeling, or behavioral analysis.In this article, we extend the architecture introduced in Part I by decomposing the monolithic synchronization layer into discrete, event-specific processors, introducing an asynchronous chart event model, and integrating a persistent event tracking subsystem.
The objective is not merely to introduce logging. It is to establish a structured mechanism through which all lifecycle events are consistently classified, routed, and permanently preserved across the full lifetime of each managed zone.
Architectural Focus of This Phase
The following structural improvements are introduced in this phase:
- Decomposition of the synchronization layer
The existing monolithic scanning mechanism is broken into discrete handlers responsible for specific zone lifecycle operations. - Transition to an event-driven execution model
Continuous polling is replaced with asynchronous processing via platform event callbacks. - Separation of interaction domains
Market-driven updates and user-driven actions are processed through independent execution pathways. - Introduction of a structured event routing layer
Incoming platform events are classified and dispatched to dedicated zone management functions. - Integration of a persistent logging subsystem
All lifecycle transitions are recorded in a structured, append-only format for long-term traceability.
One of the most important responsibilities of our framework is maintaining consistency between graphical chart objects and the internal CZone objects stored within memory. In Part I, this responsibility was achieved through polling-based synchronization. Whenever the primary execution cycle was triggered, the framework would inspect chart objects and attempt to determine whether anything had changed.
Conceptually, the process resembled the following:
void OnTick() { SyncHybridObjects(); QualifyBreakouts(); UpdateZoneVisuals(); }
This design was intentionally simple. If a trader manually created a rectangle, the synchronization layer would eventually discover the new object. If a trader modified a zone, the synchronization layer would detect the updated coordinates. If a trader removed a zone, the synchronization layer would identify the missing object and remove the associated internal representation. For small systems, this approach is common. Polling remains a frequently used technique throughout trading software development because market data naturally arrives as a continuous stream of ticks. Chart interactions, however, occur as discrete events. A trader creates a zone once. A trader deletes a zone once. A trader resizes a rectangle once.
During active trading sessions, thousands of ticks may arrive while the trader does not alter a single chart object. Despite this, a polling framework continuously asks the same questions on every single tick:
- Has a new rectangle appeared?
- Has an object been modified?
- Has an object been removed?
Because chart interactions occur as discrete, sporadic actions, they naturally fit an event-driven model. Fortunately, MetaTrader 5 provides a dedicated mechanism designed specifically for handling this type of interaction without taxing the underlying execution loop.
Introducing Native Chart EventsMetaTrader 5 exposes a built-in event handler named OnChartEvent(). Unlike polling-based synchronization, OnChartEvent() executes only when a relevant chart interaction or terminal alert occurs.
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Asynchronous event processing logic goes here }
Whenever a user performs an action on the chart, the terminal environment generates an event notification, passing an event ID alongside specific parameters that pinpoint exactly which object was interacted with. For our zone management framework, object lifecycle events are the most important. Instead of repeatedly asking Did anything change? On every single tick, the framework can now adopt an efficient event-driven mindset: Something changed. Process the event immediately. Return control.
This creates a much cleaner architecture. Market processing remains isolated inside OnTick(), while user interaction processing moves entirely into OnChartEvent().
Separating Market Events From User Events
One of the most significant architectural improvements introduced in this phase of development is the explicit separation of market-driven operations from user-driven operations. Before this change, many responsibilities accumulated inside the primary execution pathway, leading to bloated loops and increased execution risk during rapid price updates.
Instead, we split the runtime behavior into two independent execution pathways as the following flowchart shows.

Each pathway now owns a specific category of responsibilities:
- Market operations remain inside OnTick(): Functions like breakout qualification, dynamic auto-generation, and zone visual modifications depend directly upon incoming price movement and evolving market structure.
- User operations move into OnChartEvent(): Functions handling level registration, manual geometric shifts, and physical deletion depend entirely upon trader interaction.
Separating these responsibilities improves code readability, reduces redundant processing, and protects manual trader adjustments from being accidentally overwritten by automated background calculations.
Deconstructing the Synchronization Layer
To transition from a polling-based synchronization model toward an event-driven workflow, the framework must be capable of responding to individual chart interactions as they occur. Rather than repeatedly scanning chart objects to detect changes, responsibility is delegated to a set of focused management routines that react to specific lifecycle events generated within the terminal.
Under this model, object creation, modification, and removal are treated as independent events. Each event is routed to a dedicated handler responsible for maintaining consistency between the chart object and its corresponding zone representation. This approach reduces unnecessary monitoring activity while allowing the framework to react immediately when a trader performs an action on the chart.
The following sections examine the core management routines that support this event-driven synchronization layer and how they cooperate to maintain the zone lifecycle.
Manual Level Onboarding and ValidationWhenever a trader creates a new rectangle while the Expert Advisor is running, the framework must determine whether that object should participate in the zone management system. This onboarding process is performed by RegisterHybridZone(), which validates the newly created object and transforms it from a simple chart rectangle into a managed zone entity tracked by the framework.
//+------------------------------------------------------------------+ //| Registers a Raw Manual User Box into a Tracked Engine Object | //+------------------------------------------------------------------+ bool RegisterHybridZone(string obj_name) { if(ObjectFind(0, obj_name) < 0) return false; double boundary_p1 = ObjectGetDouble(0, obj_name, OBJPROP_PRICE, 0); double boundary_p2 = ObjectGetDouble(0, obj_name, OBJPROP_PRICE, 1); double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Get current volatility context safely from structural global state //--- (No CopyBuffer arrays required here anymore) double passed_atr = g_cached_atr; ENUM_ZONE_TYPE derived_type = ((boundary_p1 + boundary_p2) / 2.0 > bid) ? ZONE_RESISTANCE : ZONE_SUPPORT; CZone* new_zone = new CZone((boundary_p1 + boundary_p2) / 2.0, derived_type, TimeCurrent(), passed_atr); new_zone.name = obj_name; new_zone.is_user_zone = true; new_zone.top = MathMax(boundary_p1, boundary_p2); new_zone.bottom = MathMin(boundary_p1, boundary_p2); new_zone.height = new_zone.top - new_zone.bottom; new_zone.price_level = (new_zone.top + new_zone.bottom) / 2.0; new_zone.was_drawn = true; ObjectSetInteger(0, obj_name, OBJPROP_TIME, 1, TimeCurrent()); // Pull Time 2 forward ObjectSetString(0, obj_name, OBJPROP_TEXT, "USER"); ObjectSetInteger(0, obj_name, OBJPROP_RAY_RIGHT, true); ObjectSetInteger(0, obj_name, OBJPROP_BACK, true); ObjectSetInteger(0, obj_name, OBJPROP_FILL, true); ObjectSetInteger(0, obj_name, OBJPROP_COLOR, new_zone.GetColor()); active_zones.Add(new_zone); return true; }
As discussed in Part I, the framework represents chart rectangles as managed zone objects rather than passive graphical elements. RegisterHybridZone() serves as the onboarding point for newly created levels, extracting the required chart properties, initializing the corresponding zone instance, and registering it within the framework's internal tracking structures. Once registered, the zone becomes eligible for lifecycle management, event processing, and persistent logging.
Tracking Coordinate Modifications
Creating a zone is only the first stage of its lifecycle. Once a level has been registered, traders may choose to reposition, resize, or refine its boundaries based on evolving market conditions. Whenever these visual adjustments occur, the framework must ensure that its internal representation remains synchronized with what is displayed on the chart.
This responsibility is now handled by UpdateZoneCoordinates(). The routine retrieves the latest rectangle coordinates and updates the corresponding zone object, ensuring that all subsequent calculations operate on the most recent user-defined boundaries. By maintaining this synchronization layer, the framework avoids situations where visual objects and internal states diverge over time.
Beyond simple coordinate updates, the function also serves an important governance role within the system. Automatically generated zones originate from the framework's analytical routines and are initially classified as engine-managed levels. If a trader manually modifies an auto-generated zone, the automation layer must treat it as a deliberate intervention and preserve the change.
To preserve this intent, UpdateZoneCoordinates() promotes the modified zone from an automatically managed level to a user-managed level whenever a manual adjustment is detected. Once this transition occurs, subsequent mathematical routines will no longer overwrite the trader's modifications. In effect, ownership of the level shifts from the framework to the user, ensuring that manual refinements remain intact throughout the remainder of the session.
This distinction helps maintain a clear separation between automated analysis and discretionary trader input, allowing both workflows to coexist without conflict.
//+------------------------------------------------------------------+ //| Updates Memory Trackers Exclusively for a Modified Object Name | //+------------------------------------------------------------------+ void UpdateZoneCoordinates(string obj_name) { CZone* existing_zone = NULL; for(int z = 0; z < active_zones.Total(); z++) { CZone* zone_ptr = (CZone*)active_zones.At(z); if(zone_ptr != NULL && zone_ptr.name == obj_name) { existing_zone = zone_ptr; break; } } if(existing_zone == NULL) { //--- Guard mechanism logic safely utilizing fast matching prefixes if(StringSubstr(obj_name, 0, 2) != "S_" && StringSubstr(obj_name, 0, 2) != "R_") RegisterHybridZone(obj_name); return; } double boundary_p1 = ObjectGetDouble(0, obj_name, OBJPROP_PRICE, 0); double boundary_p2 = ObjectGetDouble(0, obj_name, OBJPROP_PRICE, 1); existing_zone.top = MathMax(boundary_p1, boundary_p2); existing_zone.bottom = MathMin(boundary_p1, boundary_p2); existing_zone.height = existing_zone.top - existing_zone.bottom; existing_zone.price_level = (existing_zone.top + existing_zone.bottom) / 2.0; double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); existing_zone.type = (existing_zone.price_level > bid) ? ZONE_RESISTANCE : ZONE_SUPPORT; ObjectSetInteger(0, obj_name, OBJPROP_COLOR, existing_zone.GetColor()); //--- Refresh visual properties immediately ObjectSetInteger(0, obj_name, OBJPROP_TIME, 1, TimeCurrent()); // Force Time 2 forward again ObjectSetInteger(0, obj_name, OBJPROP_RAY_RIGHT, true); ObjectSetInteger(0, obj_name, OBJPROP_COLOR, existing_zone.GetColor()); if(!existing_zone.is_user_zone) { existing_zone.is_user_zone = true; ObjectSetString(0, existing_zone.name, OBJPROP_TEXT, "USER"); } }
Dynamic Memory Cleanup and Purging
As zones progress through their lifecycle, situations inevitably arise where they must be retired from active management. This may occur when a trader manually deletes a level from the chart or when the framework determines that a zone is no longer valid due to changing market conditions. In either case, the system must perform more than a simple visual removal. Any internal references associated with that zone must also be cleaned up to ensure that the framework remains synchronized and memory efficient.
This responsibility is now handled by RemoveZoneByName(), which serves as the central retirement mechanism for managed zone objects. The routine searches the active tracking container for the matching zone instance and safely removes it from memory once identified.
The function iterates through the tracking array in reverse order, a defensive technique commonly used when removing elements from dynamic containers. By processing entries from the end of the collection toward the beginning, the framework avoids index shifting issues that could otherwise lead to skipped elements or invalid pointer access during deletion operations.
Before the zone is removed, the framework captures several pieces of contextual information, including its source classification, zone type, and coordinate boundaries. Preserving this information allows the logging subsystem to generate a final lifecycle record, ensuring that the removal event is fully documented even after the underlying object has been destroyed.
An additional safeguard is applied when retiring automatically generated levels. Rather than simply removing the zone and forgetting its existence, the framework records the corresponding price level within an internal blacklist container. This prevents automated detection routines from immediately rediscovering and recreating the same level on subsequent market scans. Without this protection, a deleted automated zone could repeatedly cycle through creation and removal events, introducing unnecessary noise into both the chart and the event logs.
Finally, the zone is marked as no longer being visually represented before its memory reference is released. This explicit state transition ensures that the framework treats the object as retired throughout the remainder of the cleanup process and prevents orphaned references from lingering within the management layer.
RemoveZoneByName() combines memory cleanup, lifecycle retirement, blacklist protection, and event logging. This keeps the chart, internal tracking structures, and persistent event history consistent.
//+------------------------------------------------------------------+ //| Cleans Up Internal Memory Pointers Mapping to Deleted Name | //+------------------------------------------------------------------+ void RemoveZoneByName(string obj_name) { for(int i = active_zones.Total() - 1; i >= 0; i--) { CZone* z = (CZone*)active_zones.At(i); if(z == NULL) continue; if(z.name == obj_name) { if(!z.is_user_zone) { //--- Explicitly stringify the double blacklist.Add(DoubleToString(z.price_level, _Digits)); } //--- Mark state as visually dead before clearing the reference class holder z.was_drawn = false; //--- Extract memory state info before object deletion for precise logging string z_source = z.is_user_zone ? "USER" : "AUTO"; string z_type = EnumToString(z.type); double z_top = z.top; double z_bottom = z.bottom; if(active_zones.Delete(i)) { //--- Detailed automated internal cleanup notification string details = ">>> Memory cleared for internal object tracking array handle: " + obj_name; logger.LogZone(z_source, ZE_DELETED, obj_name, z_type, z_top, z_bottom, "State:CLEANUP", details); } } } }
Routing Inbound Platform Events via OnChartEvent
With the individual zone management handlers defined, the system transitions from continuous polling to an event-driven dispatch model centered on OnChartEvent().
In this configuration, the terminal becomes the sole entry point for all user and object-level interactions. Each incoming platform event is classified by its event identifier and routed to the appropriate handler responsible for updating internal state.
Rather than embedding logic within the event loop, the function acts strictly as a routing layer. It evaluates the event type, filters irrelevant or system-generated objects, and delegates processing to specialized functions such as zone registration, coordinate updates, or lifecycle removal.
This structure ensures that the event loop remains lightweight and deterministic, while all state mutation logic is isolated within dedicated subsystems.
Each handled event is also passed through the logging interface as a structured lifecycle record. This creates a consistent mapping between platform-level interactions and internal state transitions without coupling the routing layer to persistence logic.
The result is a clean separation between:
- platform event capture
- internal state mutation
- persistence recording
where OnChartEvent() functions purely as the entry and dispatch boundary of the system.
//+------------------------------------------------------------------+ //| Asynchronous Event Processing Engine | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Capturing User-Created Zones if(id == CHARTEVENT_OBJECT_CREATE) { //--- Filter immediately using system automated structural prefixes to prevent double registration if(StringSubstr(sparam, 0, 2) == "S_" || StringSubstr(sparam, 0, 2) == "R_") return; if(ObjectGetInteger(0, sparam, OBJPROP_TYPE) == OBJ_RECTANGLE) { if(RegisterHybridZone(sparam)) { //--- Explicitly declared as pointer to resolve structural conversion error CZone *z = GetZoneByName(sparam); double z_top = (z != NULL) ? z.top : ObjectGetDouble(0, sparam, OBJPROP_PRICE, 0); double z_bottom = (z != NULL) ? z.bottom : ObjectGetDouble(0, sparam, OBJPROP_PRICE, 1); string z_type = (z != NULL) ? EnumToString(z.type) : "ZONE_SUPPORT"; //--- Detailed Logging Structure Implementation string details = StringFormat(">>> Registered user zone: %s | Bounds: %.5f - %.5f", sparam, z_top, z_bottom); logger.LogZone("USER", ZE_CREATED, sparam, z_type, z_top, z_bottom, "Strength:1.0", details); } } } //--- Capturing Zone Modifications (Drag Adjustments or Point Re-sizing) else if(id == CHARTEVENT_OBJECT_DRAG || id == CHARTEVENT_OBJECT_ENDEDIT) { if(ObjectGetInteger(0, sparam, OBJPROP_TYPE) == OBJ_RECTANGLE) { UpdateZoneCoordinates(sparam); //--- Explicitly declared as pointer to resolve structural conversion error CZone *z = GetZoneByName(sparam); if(z != NULL) { string details = StringFormat(">>> ZONE MANIPULATION Resized object: %s | New Bounds: %.5f - %.5f", sparam, z.top, z.bottom); logger.LogZone("USER", ZE_MODIFIED, sparam, EnumToString(z.type), z.top, z.bottom, "Strength:1.0", details); } ChartRedraw(0); } } //--- Capturing Deletions (Object Extracted From Chart) else if(id == CHARTEVENT_OBJECT_DELETE) { //--- Explicitly declared as pointer to resolve structural conversion error CZone *z = GetZoneByName(sparam); if(z != NULL) { string details = ">>> Deletion Detected user extraction of: " + sparam; logger.LogZone(z.is_user_zone ? "USER" : "AUTO", ZE_DELETED, sparam, EnumToString(z.type), z.top, z.bottom, "Strength:0.0", details); } //--- Memory state cleanup RemoveZoneByName(sparam); ChartRedraw(0); } }
Formalizing Zone Lifecycle Events
Once system behavior transitions to a fully event-driven architecture, a standardized and unambiguous vocabulary is required to represent all structural state transitions within the framework. Without such a constraint, reliance on free-form string messages or ad-hoc diagnostic outputs introduces long-term architectural fragility.
In distributed or modular codebases, identical semantic events may be represented inconsistently across components. This leads to classification drift, where equivalent states are logged under different identifiers. Additionally, typographical variance in string-based logging undermines deterministic parsing and renders downstream statistical or analytical processing unreliable. From an engineering perspective, this effectively eliminates the possibility of structured event aggregation.
To address this, the framework enforces a strongly typed event classification layer using a compile-time enumeration.
//--- Global Enum Flags Exposed For Core Engine Compilation enum ENUM_ZONE_EVENT { ZE_CREATED, // Level established in memory or drawn by user ZE_MODIFIED, // Boundaries or internal attributes adjusted ZE_DELETED, // Level explicitly purged from chart ZE_GHOSTED, // Level breached but monitored ZE_RESURRECTED // level reactivated after price return };
This enumeration defines a closed set of valid lifecycle states for all zone entities within the system. It functions as a strict event contract, ensuring that every structural transition is both explicit and machine-verifiable at compile time.
By constraining lifecycle representation to a fixed state space, the framework eliminates ambiguity in event interpretation and enforces deterministic behavior across all interacting subsystems.
As a result, any downstream analytical module, performance evaluator, or background aggregation process can reliably classify, count, and correlate lifecycle transitions using a unified and type-safe event schema, without reliance on string parsing or heuristic interpretation.
Why a Dedicated Logger Was NecessaryOnce lifecycle events are formalized, the next requirement is reliable and structured persistence of those events. In this framework, the logger operates as a standalone subsystem responsible strictly for event recording and storage, independent of any decision-making or trading logic. A naive design would distribute file-writing operations across multiple unrelated modules, where each event source writes directly to disk. While functionally possible, this approach introduces structural inconsistency in any event-driven system.
Embedding file I/O across multiple event emitters fragments the persistence logic. Each module must then independently manage file handles, formatting rules, column ordering for CSV output, and error handling for storage operations. This results in duplicated implementation of the same responsibility across the codebase. More critically, the log structure becomes implicitly defined by multiple sources rather than a single authoritative schema. Changing the output format (fields, column order, or serialization rules) would require updates in every emitting module. This creates unnecessary coupling between event generation and storage representation.To avoid this, persistence is isolated into a dedicated logging subsystem implemented as a single responsibility class.
This establishes a strict separation of concerns:
- Event-producing components are responsible only for emitting structured lifecycle data
- The logger is responsible only for serializing and persisting that data in a consistent format
- File system operations are centralized and not exposed to external modules
As a result, the logging subsystem acts as the single authority for event persistence, ensuring consistency of output format, reducing duplication of serialization logic, and maintaining a stable interface for all event sources.
//+------------------------------------------------------------------+ //| CLogger Class Architecture | //+------------------------------------------------------------------+ class CLogger { private: int m_file_handle; //--- Safe, Sequential CSV Line Injection Matrix void WriteCSV(string &cols[]) { if(m_file_handle == INVALID_HANDLE) return; //--- Always force file pointer to the end of the file to ensure strict appending FileSeek(m_file_handle, 0, SEEK_END); FileWrite(m_file_handle, cols[0], cols[1], cols[2], cols[3], cols[4], cols[5], cols[6], cols[7], cols[8]); FileFlush(m_file_handle); } //--- Compact Local Time Capture String string Now() { return TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS); } public: CLogger() { m_file_handle = INVALID_HANDLE; } ~CLogger() { if(m_file_handle != INVALID_HANDLE) { FileClose(m_file_handle); m_file_handle = INVALID_HANDLE; } }Designing the MasterLog Architecture
The logger is designed as a lightweight, deterministic, and isolated persistence subsystem. Its responsibility is limited strictly to event serialization and durable storage. It does not participate in decision-making, state evaluation, or any form of runtime logic beyond file output control.
At a structural level, it performs three primary functions:
- Allocation of isolated storage per runtime context
- Transformation of lifecycle events into fixed-format tabular records
- Persistent, append-only writing of those records to disk
This ensures that event persistence remains consistent, predictable, and independent of any external subsystem behavior.
1.Resource Isolation and File Allocation Strategy
To prevent file contention between multiple Expert Advisor instances, symbols, or timeframes, the logging subsystem enforces strict file partitioning at initialization time. Each execution context is mapped to a unique storage file based on its runtime identity.
This eliminates shared-file access conflicts and ensures that each instance maintains an independent event history without external interference.
//--- Isolated Multi-Asset & Timeframe File Allocation Engine void Initialize() { //--- Verify sandbox folder path exists if(!FolderCreate("DynamicSR")) Print(">>> Storage folder verified or initialized."); //--- Establish individual identity per Symbol and per Timeframe string path = "DynamicSR\\SR_Prototype_" + _Symbol + "_" + EnumToString((ENUM_TIMEFRAMES)_Period) + ".csv"; //--- Open with combined READ and WRITE access to facilitate appending without erasing data m_file_handle = FileOpen(path, FILE_WRITE|FILE_READ|FILE_CSV|FILE_ANSI|FILE_SHARE_READ|FILE_COMMON, ';'); if(m_file_handle == INVALID_HANDLE) { Print("!!! PROTOTYPE LOGGER ERROR: FileOpen failed. Code: ", GetLastError()); return; } //--- Write column matrix headers only if this is a brand new file allocation if(FileSize(m_file_handle) == 0) { string header[9] = {"Time", "Source", "Event", "Zone_ID", "Type", "Top", "Bottom", "Context", "Message"}; WriteCSV(header); } //--- Skip straight to the historical end of the file to protect old data rows FileSeek(m_file_handle, 0, SEEK_END); string startup_msg = "Startup: " + _Symbol + " (" + EnumToString((ENUM_TIMEFRAMES)_Period) + ")"; string startup_cols[9]; startup_cols[0] = Now(); startup_cols[1] = "SYSTEM"; startup_cols[2] = "STARTUP"; startup_cols[3] = "CORE"; startup_cols[4] = "NONE"; startup_cols[5] = "0.0"; startup_cols[6] = "0.0"; startup_cols[7] = "INIT"; startup_cols[8] = startup_msg; //--- Simultaneous Execution: Commit row data and output directly to chart log WriteCSV(startup_cols); Print(startup_cols[8]); }
Architectural Note: File Access Model
The use of FILE_SHARE_READ combined with FILE_COMMON enables concurrent read access by external processes without blocking write operations from the Expert Advisor runtime.
This design allows the log file to function as a live telemetry stream, accessible by external tools such as analytics dashboards, spreadsheets, or monitoring utilities, while maintaining uninterrupted append-only behavior during execution.
2. Dual-Output Multi-Channel Diagnostics
The logging subsystem exposes a single event entry point that simultaneously writes structured data to persistent storage and emits a lightweight runtime diagnostic message. This function represents the final aggregation layer of the logger, where all event attributes are normalized into a fixed schema before output.
//--- Clean Simultaneous Logging Engine void LogZone(string src, ENUM_ZONE_EVENT event_type, string zone_name, string zone_type, double top, double bottom, string context_data, string compact_msg) { string cols[9]; cols[0] = Now(); cols[1] = src; // "USER", "AUTO", or "SYSTEM" cols[2] = EnumToString(event_type); // Action state tracking enum cols[3] = zone_name; // Target chart handle object reference cols[4] = zone_type; // ZONE_SUPPORT / ZONE_RESISTANCE / NONE cols[5] = DoubleToString(top, _Digits); // High boundary price cols[6] = DoubleToString(bottom, _Digits); // Low boundary price cols[7] = context_data; // Internal metric tracking indicators (e.g. Strength) cols[8] = compact_msg; // Clean, streamlined message copy //--- 1. Commit the full row to the persistent asset file matrix WriteCSV(cols); //--- 2. Direct mirror print to Terminal using the exact same data point Print(cols[8]); } };
Behavioral Note
This interface enforces a single-source event contract: every logged entry has simultaneously persisted and emitted as a runtime diagnostic signal. The persistence path ensures structured historical reconstruction, while the terminal output provides immediate execution visibility without requiring external inspection tools.
Operational Walkthrough
This section provides verification of the persistence layer introduced in the logging subsystem.
The operational behavior of the system was previously established in part I. That section demonstrated how zone objects are created, modified, and removed through direct interaction on the chart. The figures from Part I remain the reference baseline for runtime behavior and are not repeated here.
This section focuses exclusively on the recorded output generated from those same lifecycle events.

![]()

Conclusion
By decomposing the previously monolithic synchronization layer into discrete event handlers—namely RegisterHybridZone(), UpdateZoneCoordinates(), and RemoveZoneByName() — the framework transitions from a polling-oriented design to an event-driven architecture centered around OnChartEvent().
This restructuring eliminates continuous chart-scanning loops and replaces them with targeted execution triggered only by localized state changes. As a result, system execution becomes event-sparse rather than time-sampled, improving both responsiveness and structural clarity.
Each handler is now responsible for a single category of state transition, while the logging subsystem records these transitions as immutable event entries through a centralized persistence interface. This ensures that runtime actions are not only executed but also consistently serialized into a structured historical record.
The framework therefore operates on two distinct but synchronized layers:
- A real-time event layer responsible for state transitions on the chart
- A persistence layer responsible for deterministic recording of those transitions
This separation ensures that the system state is no longer transient. Every structural modification is captured as a reproducible event trace rather than an ephemeral runtime condition.
With this foundation in place, the system is prepared for extension into quantitative logic layers. The next phase will focus on analytical processing built on top of the established event stream, including volatility aggregation, structural zone evaluation, and lifecycle state transitions such as persistence weighting, merging behavior, and reactivation conditions.
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.
From Cloud to Complex: The Vietoris-Rips Filtration in MQL5
Artificial Atom Algorithm (A3)
Features of Experts Advisors
Quantum Neural Network in MQL5 (Part III): A Virtual Quantum Processor Based on Qubits
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use