
Developing a Trading System Based on the Order Book (Part I): Indicator
Introduction
Let's recap what Depth of Market is. It is a series of pending limit orders. These orders represent the trading intentions of market participants and often do not result in an actual transaction. This is because traders have the ability to cancel their previously placed orders for various reasons. These may include changes in market conditions and the resulting loss of interest in executing the order at the previously specified price and quantity.
The value returned by the function SymbolInfoInteger(_Symbol, SYMBOL_TICKS_BOOKDEPTH) corresponds precisely to the depth of the order book and represents half of the array that will be populated with price levels to be analyzed. Half of this array is allocated for the number of limit sell orders, while the other half is for limit buy orders that have been placed. According to the documentation, for assets that do not have an order queue, the value of this property is zero. An example of this can be seen in the figure below, which shows the order book with the depth of 10, showing all available price levels.
It should be noted that depth can be obtained from the symbol and not necessarily from the market depth. Using the SymbolInfoInteger function is sufficient to retrieve the property value, without resorting to the OnBookEvent handler or related functions such as MarketBookAdd. Of course, we could arrive at the same result by counting the number of elements in the MqlBookInfo array that the OnBookEvent handler populates, as we will explore in more detail later.
You might be wondering why we should use this indicator instead of simply relying on MetaTrader 5's standard order book. Here are some key reasons:
- Optimized chart space utilization, allowing customization of histogram size and its position on the screen.
- Cleaner presentation of order book events, enhancing clarity.
- Usability in the strategy tester, with a future implementation of a disk-based storage mechanism for BookEvent events, considering that native testing is currently not supported.
Generating a Custom Symbol
This process will enable us to test the indicator even when the market is closed or when the broker does not transmit events for the given symbol. In such cases, there will be no live order queue, nor will these events be cached on the local computer. At this stage, we will not be working with past events from a real symbol but will instead focus on generating simulated BookEvent data for fictitious assets. This is necessary because creating such an asset and simulating events is essential for working with the CustomBookAdd function. This function is specifically designed for custom symbols.
Below is the CloneSymbolTicksAndRates script, which will generate the custom symbol. It has been adapted from the documentation to suit our needs and begins by defining some constants and including the standard DateTime.mqh library for working with dates. Note that the name of the custom symbol will be derived from the real symbol's nomenclature, which is passed to the script via the Symbol() function. Therefore, this script must be run on the real asset to be cloned. Although it is also possible to clone custom symbols, doing so does not seem particularly useful.
#define CUSTOM_SYMBOL_NAME Symbol()+".C" #define CUSTOM_SYMBOL_PATH "Forex" #define CUSTOM_SYMBOL_ORIGIN Symbol() #define DATATICKS_TO_COPY UINT_MAX #define DAYS_TO_COPY 5 #include <Tools\DateTime.mqh>
The following fragment, inserted into the OnStart() function of the same script, creates the "timemaster" date object. It is used to calculate the time period in which ticks and bars will be collected for cloning. According to the DAYS_TO_COPY constant we defined, the Bars function will copy the last five days of the source symbol. This same initial time of the range is then converted to milliseconds and used by the CopyTicks function, thus completing the "cloning" of the symbol.
CDateTime timemaster; datetime now = TimeTradeServer(); timemaster.Date(now); timemaster.DayDec(DAYS_TO_COPY); long DaysAgoMsc = 1000 * timemaster.DateTime(); int bars_origin = Bars(CUSTOM_SYMBOL_ORIGIN, PERIOD_M1, timemaster.DateTime(), now); int create = CreateCustomSymbol(CUSTOM_SYMBOL_NAME, CUSTOM_SYMBOL_PATH, CUSTOM_SYMBOL_ORIGIN); if(create != 0 && create != 5304) return; MqlTick array[] = {}; MqlRates rates[] = {}; int attempts = 0; while(attempts < 3) { int received = CopyTicks(CUSTOM_SYMBOL_ORIGIN, array, COPY_TICKS_ALL, DaysAgoMsc, DATATICKS_TO_COPY); if(received != -1) { if(GetLastError() == 0) break; } attempts++; Sleep(1000); }
Once the process is complete, the new symbol should appear in the market watch list with the name <AtivodeOrigem>.C. At this point, we need to open a new chart with this synthetic symbol and proceed to the next step.
If another synthetic symbol already exists, it can be reused, making it unnecessary to create a new one as explained in this section. In the end, we simply need to open a new chart with this custom symbol and run two other MQL5 applications that we will develop here: the indicator and the event generator script. We will provide all the details in the following sections.
BookEvent-Type Event Generator Script for Testing
Having a custom symbol alone does not compensate for the absence of an online order book tick sequence when performing a backtest on the indicator that relies on order book events. Therefore, we need to generate simulated data. For this purpose, the following script has been developed.
//+------------------------------------------------------------------+ //| GenerateBookEvent.mq5 | //| Daniel Santos | //+------------------------------------------------------------------+ #property copyright "Daniel Santos" #property version "1.00" #define SYNTH_SYMBOL_MARKET_DEPTH 32 #define SYNTH_SYMBOL_BOOK_ITERATIONS 20 #include <Random.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double BidValue, tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); MqlBookInfo books[]; int marketDepth = SYNTH_SYMBOL_MARKET_DEPTH; CRandom rdn; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { if(!SymbolInfoInteger(_Symbol, SYMBOL_CUSTOM)) // if the symbol exists { Print("Custom symbol ", _Symbol, " does not exist"); return; } else BookGenerationLoop(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void BookGenerationLoop() { MqlRates BarRates_D1[]; CopyRates(_Symbol, PERIOD_D1, 0, 1, BarRates_D1); if(ArraySize(BarRates_D1) == 0) return; BidValue = BarRates_D1[0].close; ArrayResize(books, 2 * marketDepth); for(int j = 0; j < SYNTH_SYMBOL_BOOK_ITERATIONS; j++) { for(int i = 0, j = 0; i < marketDepth; i++) { books[i].type = BOOK_TYPE_SELL; books[i].price = BidValue + ((marketDepth - i) * tickSize); books[i].volume_real = rdn.RandomInteger(10, 500); books[i].volume_real = round((books[i].volume_real + books[j].volume_real) / 2); books[i].volume = (int)books[i].volume_real; //---- books[marketDepth + i].type = BOOK_TYPE_BUY; books[marketDepth + i].price = BidValue - (i * tickSize); books[marketDepth + i].volume_real = rdn.RandomInteger(10, 500); books[marketDepth + i].volume_real = round((books[marketDepth + i].volume_real + books[marketDepth + j].volume_real) / 2); books[marketDepth + i].volume = (int)books[marketDepth + i].volume_real; if(j != i) j++; } CustomBookAdd(_Symbol, books); Sleep(rdn.RandomInteger(400, 1000)); } } //+------------------------------------------------------------------+
Instead of the standard MathRand() function, we used an alternative implementation for generating 32-bit random numbers. This choice was made for several reasons, including the ease of generating integer values within a specified range - an advantage we leveraged in this script by using the RandomInteger(min, max) function.
For the order book depth, we selected a relatively large value of 32, meaning that each iteration will generate 64 price levels. If needed, this value can be adjusted to a smaller one.
The algorithm first checks whether the symbol is a custom one. If it is, it proceeds to generate each element of the order book and repeats this process in another loop based on the specified number of iterations. In this implementation, 20 iterations are performed with randomly chosen pauses between 400 milliseconds and 1000 milliseconds (equivalent to 1 second). This dynamic approach makes the visualization of ticks more realistic and visually appealing.
Prices are vertically anchored to the last closing price of the daily timeframe, as indicated by the source symbol. Above this reference point, there are 32 levels of sell orders, while below it, there are 32 levels of buy orders. According to the indicator's standard color scheme, histogram bars corresponding to sell orders have a reddish hue, while buy orders are represented in light blue.
The price difference between consecutive levels is determined based on the tick size of the symbol, which is obtained through the SYMBOL_TRADE_TICK_SIZE property.
Indicator for Displaying Market Depth Changes
Library Source Code
The indicator was developed using object-oriented programming. The BookEventHistogram class was created to manage the order book histogram, handling its creation, updates, and the removal of bars when the class object is destroyed.
Below are the variable and function declarations for the BookEventHistogram class:
class BookEventHistogram { protected: color histogramColors[]; //Extreme / Mid-high / Mid-low int bookSize; int currElements; int elementMaxPixelsWidth; bool showMessages; ENUM_ALIGN_MODE corner; string bookEventElementPrefix; public: MqlBookInfo lastBook[]; datetime lastDate; void SetAlignLeft(void); void SetCustomHistogramColors(color &colors[]); void SetBookSize(int value) {bookSize = value;} void SetElementMaxPixelsWidth(int m); int GetBookSize(void) {return bookSize;} void DrawBookElements(MqlBookInfo& book[], datetime now); void CleanBookElements(void); void CreateBookElements(MqlBookInfo& book[], datetime now); void CreateOrRefreshElement(int buttonHeigh, int buttonWidth, int i, color clr, int ydistance); //--- Default constructor BookEventHistogram(void); ~BookEventHistogram(void); };
Not all functions are defined in this segment; however, they are completed in the remaining lines of the BookEventHistogram.mqh file.
Among the most important functions, CreateBookElements and CreateOrRefreshElement work together to ensure that existing elements are updated while creating new ones when necessary. The remaining functions serve to keep properties up to date or to return the values of certain object variables.
Source code of the indicator:
The beginning of the code defines the number of plots and buffers as 3. A deeper analysis will reveal that, in reality, the root structure buffers of an MQL5 indicator are not used. However, this declaration facilitates the generation of code that ensures user interaction with certain properties during the indicator's initialization. In this case, our focus is on color properties, where the input scheme is designed to provide an intuitive and user-friendly experience.
Each plot is assigned two colors - one for buy orders and one for sell orders. This set of six colors is used to determine the color of each segment based on predefined criteria. Broadly speaking, the largest segments in the histogram are classified as "extremes", those above the average size as "mid-high", and the rest as "mid-low".
Colors are retrieved using the PlotIndexGetInteger function, which specifies the plot and the position within the plot from which the information should be extracted.
#define NUMBER_OF_PLOTS 3 #property indicator_chart_window #property indicator_buffers NUMBER_OF_PLOTS #property indicator_plots NUMBER_OF_PLOTS //--- Invisible plots #property indicator_label1 "Extreme volume elements colors" #property indicator_type1 DRAW_NONE #property indicator_color1 C'212,135,114', C'155,208,226' //--- #property indicator_label2 "Mid-high volume elements colors" #property indicator_type2 DRAW_NONE #property indicator_color2 C'217,111,86', C'124,195,216' //--- #property indicator_label3 "Mid-low volume elements color" #property indicator_type3 DRAW_NONE #property indicator_color3 C'208,101,74', C'114,190,214' #include "BookEventHistogram.mqh" enum HistogramPosition { LEFT, //<<<< Histogram on the left RIGHT, //Histogram on the right >>>> }; enum HistogramProportion { A_QUARTER, // A quarter of the chart A_THIRD, // A third of the chart HALF, // Half of the chart }; input HistogramPosition position = RIGHT; // Indicator position input HistogramProportion proportion = A_QUARTER; // Histogram ratio (compared to chart width) //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ double volumes[]; color histogramColors[]; BookEventHistogram bookEvent;
Next, we introduce two enumerators designed to provide users with precise options when loading the indicator. We want to determine where the histogram should be drawn: on the right or left side of the chart. Additionally, the user must specify the proportion of the chart width that the histogram will occupy: one-fourth, one-third, or half of the chart. For instance, if the chart width is 500 pixels and the user selects the half-width option, histogram bars can range in size from 0 to 250 pixels.
Finally, in the source code BookEvents.mq5, the OnBookEvent and OnChartEvent functions will trigger most of the histogram update requests. The OnCalculate function does not play a role in the algorithm and is only retained for MQL syntax compliance.
Using the Scripts and Indicator
The correct sequence for running the scripts and the indicator, ensuring consistency with the resources developed so far, is as follows:
- Run the script CloneSymbolTicksAndRates on the chart of the real symbol to be cloned.
- -> BookEvents indicator (on the chart of the generated custom symbol)
- -> GenerateBookEvent script (on the chart of the generated custom symbol)
The BookEvent is broadcast to all graphical instances of the targeted custom asset. Therefore, the indicator and event generator script can be executed on separate charts or within the same chart, as long as they reference the same custom symbol.
The animation below illustrates this sequence as well as the functionality of the indicator. I hope you enjoy it!
Conclusion
Depth of Market is undoubtedly a very important element for executing fast trades, especially in High Frequency Trading (HFT) algorithms. It is a type of market event that brokers provide for many trading symbols. Over time, brokers may expand the coverage and availability of such data for additional assets.
However, I believe it is not advisable to build a trading system solely based on the order book. Instead, the DOM can help identify liquidity zones and may exhibit some correlation with price movements. Therefore, combining order book analysis with other tools and indicators is a prudent approach to achieving consistent trading results.
There is room for future enhancements to the indicator, such as implementing mechanisms to store BookEvent data and later use them in backtesting, both for manual trading and for automated strategies.
Translated from Portuguese by MetaQuotes Ltd.
Original article: https://www.mql5.com/pt/articles/15748





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
This is very short article with very little code. Let see in next part if it makes sense have part 1, 2 and so on.
SYMBOL_TICKS_BOOKDEPTH gives the maximal number of requests shown in Depth of Market. Is incorrect that this property gives the same result as counting the number of levels in the DOM. It gives the maximal number not precise number.
You can very that using this script: