English Español Deutsch 日本語 Português
preview
Разработка системы репликации (Часть 69): Настройка времени (II)

Разработка системы репликации (Часть 69): Настройка времени (II)

MetaTrader 5Примеры | 18 марта 2025, 08:59
286 0
Daniel Jose
Daniel Jose

Введение

В предыдущей статье, "Разработка системы репликации (часть 68): Настройка времени (I)", была объяснена часть кода, которая связана с указателем мыши. Но данный код не имеет никакой ценности, если не проверен код сервиса репликации/моделирования. Однако, если вы не читали предыдущую статью, то я советую вам прочитать её, прежде чем пытаться понять данную статью, ведь они дополняют друг друга.

Пока что уделим основное внимание сообщению об оставшемся времени бара, при котором символ имеет низкую ликвидность. Такое происходит, потому что в такие моменты события OnCalculate не будут генерироваться спонтанно или, другими словами, традиционным способом. Таким образом, указатель мыши вообще не будет получать правильные значения прошедших секунд. Однако, основываясь на предыдущей статье, можно передать значения, необходимые указателю для подсчета количества оставшихся секунд.

На этом первом этапе мы сосредоточимся в основном на сервисе репликации/моделирования. Если говорить более конкретно, то мы сосредоточимся на файле C_Replay.mqh. А теперь давайте приступим к анализу изменений и дополнений, внесенных в код.


Настраиваем файл C_Replay.mqh

Изменений, которые необходимо внести, не так много. Но благодаря им описанный в предыдущей статье код приобретет смысл, особенно та его часть, которая связана с использованием функции библиотеки iSpread в событии OnCalculate. Вы, наверное, задались вопросом, почему мы использовали функцию iSpread, ведь практичнее было бы считывать значение спреда непосредственно из массива, полученного функцией OnCalculate.

На самом деле, это довольно интересно. Однако, чтобы понять причины, нужно разобраться, как всё устроено на самом деле. Для этого необходимо рассмотреть и понять, как код сервиса репликации/моделирования выполняет свои функции. И помимо этого, конечно, мы должны понимать, как MetaTrader 5 обрабатывает информацию.

Начнем с самого простого: понять код в файле C_Replay.mqh. Это важно, поскольку этот файл отвечает за генерацию информации, представленной на графике. Модифицированный код в полном виде можно увидеть ниже:

001. //+------------------------------------------------------------------+
002. #property copyright "Daniel Jose"
003. //+------------------------------------------------------------------+
004. #include "C_ConfigService.mqh"
005. #include "C_Controls.mqh"
006. //+------------------------------------------------------------------+
007. #define def_IndicatorControl   "Indicators\\Market Replay.ex5"
008. #resource "\\" + def_IndicatorControl
009. //+------------------------------------------------------------------+
010. #define def_CheckLoopService ((!_StopFlag) && (ChartSymbol(m_Infos.IdReplay) != ""))
011. //+------------------------------------------------------------------+
012. #define def_ShortNameIndControl    "Market Replay Control"
013. #define def_MaxSlider             (def_MaxPosSlider + 1)
014. //+------------------------------------------------------------------+
015. class C_Replay : public C_ConfigService
016. {
017.    private   :
018.       struct st00
019.       {
020.          C_Controls::eObjectControl Mode;
021.          uCast_Double               Memory;
022.          ushort                     Position;
023.          int                        Handle;
024.       }m_IndControl;
025.       struct st01
026.       {
027.          long     IdReplay;
028.          int      CountReplay;
029.          double   PointsPerTick;
030.          MqlTick  tick[1];
031.          MqlRates Rate[1];
032.       }m_Infos;
033.       stInfoTicks m_MemoryData;
034. //+------------------------------------------------------------------+
035. inline bool MsgError(string sz0) { Print(sz0); return false; }
036. //+------------------------------------------------------------------+
037. inline void UpdateIndicatorControl(void)
038.          {
039.             double Buff[];
040.                                  
041.             if (m_IndControl.Handle == INVALID_HANDLE) return;
042.             if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position)
043.             {
044.                if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1)
045.                   m_IndControl.Memory.dValue = Buff[0];
046.                if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay)
047.                   m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition];
048.             }else
049.             {
050.                m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position;
051.                m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode;
052.                m_IndControl.Memory._8b[7] = 'D';
053.                m_IndControl.Memory._8b[6] = 'M';
054.                EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, "");
055.             }
056.          }
057. //+------------------------------------------------------------------+
058.       void SweepAndCloseChart(void)
059.          {
060.             long id;
061.             
062.             if ((id = ChartFirst()) > 0) do
063.             {
064.                if (ChartSymbol(id) == def_SymbolReplay)
065.                   ChartClose(id);
066.             }while ((id = ChartNext(id)) > 0);
067.          }
068. //+------------------------------------------------------------------+
069. inline void CreateBarInReplay(bool bViewTick)
070.          {
071.             bool    bNew;
072.             double dSpread;
073.             int    iRand = rand();
074.             static int st_Spread = 0;
075. 
076.             if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew))
077.             {
078.                m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay];
079.                if (m_MemoryData.ModePlot == PRICE_EXCHANGE)
080.                {                  
081.                   dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
082.                   if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
083.                   {
084.                      m_Infos.tick[0].ask = m_Infos.tick[0].last;
085.                      m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
086.                   }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
087.                   {
088.                      m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
089.                      m_Infos.tick[0].bid = m_Infos.tick[0].last;
090.                   }
091.                }
092.                if (bViewTick)
093.                {
094.                   CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
095.                   if (bNew) EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)m_Infos.Rate[0].time, 0, "");
096.                }
097.                st_Spread = (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time);
098.                m_Infos.Rate[0].spread = (int)macroGetSec(m_MemoryData.Info[m_Infos.CountReplay].time);
099.                CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate);
100.             }
101.             m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread);
102.             CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate);
103.             m_Infos.CountReplay++;
104.          }
105. //+------------------------------------------------------------------+
106.       void AdjustViewDetails(void)
107.          {
108.             MqlRates rate[1];
109. 
110.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
111.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
112.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE);
113.             m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
114.             CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate);
115.             if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE))
116.                for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++);
117.             if (rate[0].close > 0)
118.             {
119.                if (GetInfoTicks().ModePlot == PRICE_EXCHANGE)
120.                   m_Infos.tick[0].last = rate[0].close;
121.                else
122.                {
123.                   m_Infos.tick[0].bid = rate[0].close;
124.                   m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick);
125.                }               
126.                m_Infos.tick[0].time = rate[0].time;
127.                m_Infos.tick[0].time_msc = rate[0].time * 1000;
128.             }else
129.                m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay];
130.             CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
131.          }
132. //+------------------------------------------------------------------+
133.       void AdjustPositionToReplay(void)
134.          {
135.             int nPos, nCount;
136.             
137.             if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return;
138.             nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider);
139.             for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread);
140.             if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1);
141.             while ((nPos > m_Infos.CountReplay) && def_CheckLoopService)
142.                CreateBarInReplay(false);
143.          }
144. //+------------------------------------------------------------------+
145.    public   :
146. //+------------------------------------------------------------------+
147.       C_Replay()
148.          :C_ConfigService()
149.          {
150.             Print("************** Market Replay Service **************");
151.             srand(GetTickCount());
152.             SymbolSelect(def_SymbolReplay, false);
153.             CustomSymbolDelete(def_SymbolReplay);
154.             CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay));
155.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0);
156.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0);
157.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0);
158.             CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation");
159.             CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8);
160.             SymbolSelect(def_SymbolReplay, true);
161.             m_Infos.CountReplay = 0;
162.             m_IndControl.Handle = INVALID_HANDLE;
163.             m_IndControl.Mode = C_Controls::ePause;
164.             m_IndControl.Position = 0;
165.             m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState;
166.          }
167. //+------------------------------------------------------------------+
168.       ~C_Replay()
169.          {
170.             SweepAndCloseChart();
171.             IndicatorRelease(m_IndControl.Handle);
172.             SymbolSelect(def_SymbolReplay, false);
173.             CustomSymbolDelete(def_SymbolReplay);
174.             Print("Finished replay service...");
175.          }
176. //+------------------------------------------------------------------+
177.       bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate)
178.          {
179.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0)
180.                return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket.");
181.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0)
182.                return MsgError("Asset configuration is not complete, need to declare the ticket value.");
183.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0)
184.                return MsgError("Asset configuration not complete, need to declare the minimum volume.");
185.             SweepAndCloseChart();
186.             m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1);
187.             if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl"))
188.                Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl");
189.             else
190.                Print("Apply template: ", szNameTemplate, ".tpl");
191. 
192.             return true;
193.          }
194. //+------------------------------------------------------------------+
195.       bool InitBaseControl(const ushort wait = 1000)
196.          {
197.             Print("Waiting for Mouse Indicator...");
198.             Sleep(wait);
199.             while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200);
200.             if (def_CheckLoopService)
201.             {
202.                AdjustViewDetails();
203.                Print("Waiting for Control Indicator...");
204.                if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false;
205.                ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle);
206.                UpdateIndicatorControl();
207.             }
208.             
209.             return def_CheckLoopService;
210.          }
211. //+------------------------------------------------------------------+
212.       bool LoopEventOnTime(void)
213.          {         
214.             int iPos;
215. 
216.             while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay))
217.             {
218.                UpdateIndicatorControl();
219.                Sleep(200);
220.             }
221.             m_MemoryData = GetInfoTicks();
222.             AdjustPositionToReplay();
223.             EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)macroRemoveSec(m_MemoryData.Info[m_Infos.CountReplay].time), 0, "");
224.             iPos = 0;
225.             while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService))
226.             {
227.                if (m_IndControl.Mode == C_Controls::ePause) return true;
228.                iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0);
229.                CreateBarInReplay(true);
230.                while ((iPos > 200) && (def_CheckLoopService))
231.                {
232.                   Sleep(195);
233.                   iPos -= 200;
234.                   m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks);
235.                   UpdateIndicatorControl();
236.                }
237.             }
238. 
239.             return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService));
240.          }
241. };
242. //+------------------------------------------------------------------+
243. #undef def_SymbolReplay
244. #undef def_CheckLoopService
245. #undef def_MaxSlider
246. //+------------------------------------------------------------------+

Исходный код файла C_Replay.mqh

В приведенном коде видно, что некоторые строки зачеркнуты. Их следует удалить из кода, существовавшего до этой статьи. Количество строк не очень большое, но последствия будут огромными.

Первое, что нужно отметить - это появление новой переменной в строке 74. Её цель довольно проста: считать секунды, когда ликвидность резко снизится или станет слишком низкой. Хотя такая операция не проводится в данный момент, важно разбираться в том, что происходит для понимания того, как она будет реализована. 

Во-первых, прошу заметить, что пользовательское событие было удалено из исходного кода в строке 223. Но обратите внимание на один важный момент: в каждой итерации цикла, начиная со строки 225, происходит вызов CreateBarInReplay. Это осуществляется в строке 229. Теперь обратите внимание на следующее: примерно каждые 195 миллисекунд, благодаря строке 232 и времени, необходимому для выполнения вызовов цикла в строке 225, будет выполняться вызов CreateBarInReplay. Это означает, что в секунду будет происходить около пяти вызовов, если между ними нет других вызовов. В этот момент мы должны забыть о том, что мы находимся в периоде хорошей ликвидности. Я пытаюсь показать, как будет работать сервис репликации/моделирования при очень низкой ликвидности. Поэтому сохраните эти данные: примерно пять вызовов в секунду к функции CreateBarInReplay.

Теперь вернемся к процедуре CreateBarInReplay, чтобы понять, что происходит при достаточной ликвидности, т.е. когда каждую секунду совершается не менее пяти вызовов.

В этом случае условие в строке 76 будет равно true. Поэтому будет выполнено содержимое между строками 77 и 100, но примите во внимание, что в данном диапазоне есть несколько строк, которые были удалены из кода. Можно проверить это, посмотрев на зачеркнутые строки. Среди этих строк - 95, которая генерирует пользовательское событие каждый раз, когда создается новый одноминутный бар. Это будет иметь решающее значение для появления функции iSpread в процедуре OnCalculate, но пока что не беспокойтесь об этом. Для начала давайте разберемся в основных моментах. Обратите внимание, что в строке 97 добавлен новый код, который инициализирует значение переменной.

Теперь будьте очень внимательны: вы увидите, что строки 98 и 99 зачекрнуты. Однако код не исчез, а просто изменил положение. Ранее данные строки находились внутри блока кода, который выполнялся, если условие в строке 76 было истинным. Теперь их выполнение является безусловным, поскольку они были переданы на строки 101 и 102. Но обратите внимание: хотя строка 101 отличается, она выполняет то же действие, что и строка 98, только теперь в нее включена маска. Это позволяет указателю мыши знать, что значение спреда поступает из сервиса репликации/моделирования. Мы просто используем операцию OR, чтобы правильно настроить маску. Однако это создает проблему: если значение переменной st_Spread вторгнется в битовую область, зарезервированную для маски, указатель мыши не сможет правильно интерпретировать получаемые значения.

Если что-то выглядит странно или не получается, можно просто проверить, не заходит ли значение переменной st_Spread в битовую область, зарезервированную под маску. Однако в обычных ситуациях этого не должно происходить, поскольку сервис репликации/моделирования предназначен для внутридневных исследований и анализа. Такое состояние может возникнуть только в том случае, если сервис репликации/моделирования будет доведен до крайнего предела своей производительности по времени. Однако, чтобы избежать неудобств, могу заранее предупредить о том, что временной лимит составляет почти 12 дней в пересчете на секунды, и этого более чем достаточно для нашей цели.

Давайте продолжим наше объяснение того, как работает данная система. Если скомпилировать и запустить сервис репликации/моделирования вместе со скомпилированным кодом указателя мыши, представленным в предыдущей статье, а символ будет иметь правильную ликвидность (то есть хотя бы один тик в секунду), то получим правильное обновление времени, оставшегося до окончания текущего бара и начала нового.

Хорошо, но это не объясняет того, почему не был использован массив спредов, присутствующий в одной из версий вызова OnCalculate. Почему нужно было использовать iSpread для получения значения спреда, который предоставляет сервис, как показано в строке 101? Чтобы понять это, необходимо разобраться еще в одном моменте.


Почему стоит использовать iSpread?

На момент написания статьи последней версией MetaTrader 5 была та, которая показана ниже:

Image 01

Возможно, в тот момент, когда вы читаете данную статью, MetaTrader 5 всё еще обрабатывает бары, по крайней мере, в части пользовательских символов, в своей своеобразной манере. Возможно, не вся информация относится к бару, но поскольку мы передаем информацию через спред, это своеобразное поведение может быть проверено.

Чтобы продемонстрировать это, мы внесем небольшие изменения в код заголовочного файла C_Replay.mqh и в указатель мыши. Думаю, так будет проще понять, что происходит на самом деле, ведь просто объяснить это будет недостаточно. Поэтому в файле C_Replay.mqh изменим код в следующем фрагменте:

068. //+------------------------------------------------------------------+
069. inline void CreateBarInReplay(bool bViewTick)
070.          {
071.             bool    bNew;
072.             double dSpread;
073.             int    iRand = rand();
074.             static int st_Spread = 0;
075. 
076.             if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew))
077.             {
078.                m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay];
079.                if (m_MemoryData.ModePlot == PRICE_EXCHANGE)
080.                {                  
081.                   dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
082.                   if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
083.                   {
084.                      m_Infos.tick[0].ask = m_Infos.tick[0].last;
085.                      m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
086.                   }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
087.                   {
088.                      m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
089.                      m_Infos.tick[0].bid = m_Infos.tick[0].last;
090.                   }
091.                }
092.                if (bViewTick)
093.                   CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
094.                st_Spread = (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time);
095.             }
096.             Print(TimeToString(st_Spread, TIME_SECONDS));
097.             m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread);
098.             CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate);
099.             m_Infos.CountReplay++;
100.          }
101. //+------------------------------------------------------------------+

Фрагмент из файла C_Replay.mqh

Обратите внимание, что код в данном фрагменте уже отлажен, поэтому нумерация строк может немного отличаться. Однако код будет идентичен тому, что было показано ранее в этой статье. Разница заключается в строке 96, которая была добавлена, чтобы показать в терминале значение, которое мы присваиваем спреду бара. В результате выполнения данного модифицированного кода мы получим следующее:

Anime 01

Обратите внимание, что отображаемое значение точно такое же, как и значение, которое показывается в виде времени на тиковом графике. Очень важно понять это. Теперь у нас есть подтверждение того, что значение, размещаемое на спреде бара, на самом деле является временным значением на графике. Теперь рассмотрим другой аспект. Давайте немного изменим индикатор управления, это будут едва уловимые изменения, чтобы проанализировать, как работает система. Данную модификацию внесем в код заголовочного файла C_Study.mqh. Она выглядит так:

109. //+------------------------------------------------------------------+
110.       void Update(const eStatusMarket arg)
111.          {
112.             int i0;
113.             datetime dt;
114.                      
115.             switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status))
116.             {
117.                case eCloseMarket :
118.                   m_Info.szInfo = "Closed Market";
119.                   break;
120.                case eInReplay    :
121.                case eInTrading   :
122.                   i0 = PeriodSeconds();
123.                   dt = (m_Info.Status == eInReplay ? (datetime) GL_TimeAdjust : TimeCurrent());
124.                   m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time);
125.                   if (dt > 0) m_Info.szInfo = TimeToString((datetime)m_Info.Rate.time/* - dt*/, TIME_SECONDS);
126.                   break;
127.                case eAuction     :
128.                   m_Info.szInfo = "Auction";
129.                   break;
130.                default           :
131.                   m_Info.szInfo = "ERROR";
132.             }
133.             Draw();
134.          }
135. //+------------------------------------------------------------------+

Фрагмент файла C_Study.mqh

Смотрите очень внимательно, ведь изменения очень тонкие. В строке 125 удалили настройку dt, так что теперь будет отображаться информация о том, когда ожидается новый бар. Прошу заметить: речь идет не о времени, оставшемся до появления нового бара, а о том, когда он действительно появится. Затем мы снова компилируем указатель мыши, чтобы проверить значение, которое будет отображаться. На следующей анимации можно увидеть, что происходит на самом деле.

Anime 2

Обратите внимание, что используемый графический период составляет две минуты. Таким образом, расчет показывает, когда появится новый бар, это отображается в указателе мыши. Обратите внимание, что когда период графика достигает соответствующей позиции, индикатор автоматически начинает сообщать, когда появится новый бар, т.е. система работает правильно. Однако в этих тестах мы не проверили, какое значение предоставляет сервис репликации/моделирования. Мы просто проверяем информацию, которая, как нам было известно, должна была находиться там. Теперь мы проверим передаваемое им значение. Мы должны убедиться, что графический период не равен одной минуте, иначе тест не будет действительным. Мы оставим период в две минуты: этого достаточно для анализа происходящего.

Для того, чтобы тест прошел правильно, необходимо внести небольшие изменения. Еще раз прошу обратить внимание на код в следующем фрагменте.

109. //+------------------------------------------------------------------+
110.       void Update(const eStatusMarket arg)
111.          {
112.             int i0;
113.             datetime dt;
114.                      
115.             switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status))
116.             {
117.                case eCloseMarket :
118.                   m_Info.szInfo = "Closed Market";
119.                   break;
120.                case eInReplay    :
121.                case eInTrading   :
122.                   i0 = PeriodSeconds();
123.                   dt = (m_Info.Status == eInReplay ? (datetime) GL_TimeAdjust : TimeCurrent());
124.                   m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time);
125.                   if (dt > 0) m_Info.szInfo = TimeToString((datetime)/*m_Info.Rate.time -*/ dt, TIME_SECONDS);
126.                   break;
127.                case eAuction     :
128.                   m_Info.szInfo = "Auction";
129.                   break;
130.                default           :
131.                   m_Info.szInfo = "ERROR";
132.             }
133.             Draw();
134.          }
135. //+------------------------------------------------------------------+

Фрагмент файла C_Study.mqh

Теперь мы выделяем значение, предоставляемое сервисом, и отображаем его в указателе мыши. Результат можно увидеть ниже:

Anime 3

Как видно, это в точности соответствует ожиданиям. Теперь мы не будем изменять данный код в заголовочном файле, но выполним другое действие внутри указателя мыши. Давайте посмотрим, что произойдет, если мы будем использовать значение спреда, полученное во время вызова OnCalculate. Для этого нам нужно изменить код указателя мыши. Но не забудьте: значение, которое будет отображаться в индикаторе, - это значение, захваченное и сохраненное в переменной GL_TimeAdjust. Очень важно помнить об этом. Поэтому мы перейдем к изменению кода индикатора, чтобы проверить, уместно ли использовать значение спреда, полученное при вызове OnCalculate. Новый код выглядит так:

46. //+------------------------------------------------------------------+
47. int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[],
48.                 const double& high[], const double& low[], const double& close[], const long& tick_volume[], 
49.                 const long& volume[], const int& spread[])
50. //int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double& price[])
51. {
52.    GL_PriceClose = close[rates_total - 1];
53. //   GL_PriceClose = price[rates_total - 1];
54.    GL_TimeAdjust = (spread[rates_total - 1] & (~def_MaskTimeService);
55. //   if (_Symbol == def_SymbolReplay)
56. //      GL_TimeAdjust = iSpread(NULL, PERIOD_M1, 0) & (~def_MaskTimeService);
57.    m_posBuff = rates_total;
58.    (*Study).Update(m_Status);   
59.    
60.    return rates_total;
61. }
62. //+------------------------------------------------------------------+

Фрагмент файла указателя мыши

Обратите внимание, что именно мы делаем в приведенном выше отрывке. Мы изолируем код, поэтому зачеркнутые строки соответствуют текущему коду так, как было показано в предыдущей статье, и должны быть временно удалены. Вместо этого мы ввели другой код, на этот раз для использования данных, предоставляемых MetaTrader 5, в функции OnCalculate. То есть теперь мы перестали использовать вызов iSpread. Мы будем использовать значение, которое MetaTrader 5 предоставляет в массиве спреда. Чтобы сохранить совместимость с сервисом, мы внесли небольшое изменение, как видно в строке 54. Обратите внимание, что данная операция похожа на ту, что мы делали раньше при использовании iSpread, но теперь используемое значение берется из аргумента вызова OnCalculate. Для нас это будет иметь большое значение, так как избавит нас от необходимости осуществить дополнительный вызов, чтобы получить значение, которое MetaTrader 5 уже предоставляет в качестве аргумента в OnCalculate.

Далее мы увидим результат выполнения этого кода. Он показан в следующей анимации :

Anime 4

Но что здесь произошло? Почему значение указателя мыши замерло? Ответить на эти вопросы непросто. Вопреки мнению некоторых, я не знаю точного ответа. Ну как возможно такое, что я не знаю ответа? Правда в том, что у меня есть некоторые подозрения, но я предпочитаю хранить молчание между подозрением и утверждением. Я считаю более уместным показать вам то, чего вы, возможно, не ожидали, и что всё-таки произошло. Затем вы сможете проверить это сами и сделать собственные выводы.

Однако сервис по-прежнему отправляет значения тем же способом, который был показан в предыдущих анимациях. Индикатор продолжает фиксировать значение спреда. Однако почему значение застыло в индикаторе? Хорошо, я не могу этого объяснить. Я точно знаю, что при появлении на графике нового бара, значение спреда правильно обновляется (поэтому для обнаружения данного сбоя необходимо работать с периодом, отличающимся от одной минуты).

Давайте еще раз вспомним: то, что показано здесь может не произойти к тому времени, когда вы, дорогие читатели, будете читать эту статью. Это связано с тем, что MetaTrader 5, возможно, уже получил обновление для устранения данной проблемы. Пока же я использую функцию iSpread в качестве промежуточного решения, но как только эта небольшая проблема будет устранена, мы перестанем использовать вызов iSpread и будем использовать значение, предоставляемое MetaTrader 5 функции OnCalculate. Поэтому не привязывайтесь к какой-либо части кода, так как он будет постоянно улучшаться и оптимизироваться по мере продвижения. Теперь, я думаю, вы понимаете, почему я использую функцию iSpread вместо того, чтобы использовать значение, предоставленное в качестве аргумента в вызове OnCalculate. Но мы ещё не закончили. Нам еще предстоит придумать, как сделать так, чтобы сервис сообщал нам об оставшемся времени бара, когда низкая ликвидность означает, что у нас нет тиков, или, скорее, событий, которые вызывают функцию OnCalculate каждую секунду. Для этого мы рассмотрим изменения, внесенные в данный аспект, и объясним, почему в индикаторе используется функция iSpread. После этого мы возобновим работу над сервисом.


Устраняем сбой в работе сервиса

К сожалению, чтобы всё работало, когда время между тиками больше одной секунды, нам придется подойти к проблеме не так, как планировалось в начале статьи. Дело в том, что мы хотели разместить счетчик внутри процедуры создания бара. Однако мы упустили одну важную деталь: время. Давайте вернемся к началу статьи и просмотрим исходный код заголовочного файла C_Replay.mqh. В строке 230 находится цикл, который останавливает работу сервиса репликации/моделирования на определенное время, пока не пройдет необходимое время для появления нового тика. И вот тут-то и возникает проблема.

Проблема в том, что всё это время мы тестировали сервис репликации/моделирования на символах с хорошей ликвидностью, то есть символы, которые в истории имели время между тиками менее одной секунды. Возникла ошибка при запуске изменений, учитывающих возможность тиков длительностью более одной секунды. Она появилась не внезапно. На самом деле она уже была там, но из-за короткого промежутка времени между тиками осталась незамеченной. Теперь я прошу обратить особое внимание на цикл между строками 230 и 236. Что там не так? Ошибка заключается в том, что мы неправильно управляем паузой, которую пользователь может захотеть применить к системе. Как это происходит? Если сервис находится в цикле, ожидая времени до следующего тика, то в чем заключается проблема? Действительно, нет никаких проблем, если время ожидания невелико. Однако, когда время ожидания превышает одну секунду, возникает серьезная проблема.

Предположим, что мы воспроизводим данные Forex. В начале дневной серии время между тиками обычно значительно велико. Если мы запустим сервис и он обнаружит, что для достижения следующего тика ему нужно подождать 40 секунд, даже если мы сделаем паузу и переместим пин индикатора управления в другое положение, а затем возобновим работу сервиса, он не ответит на команду "возобновить". И это происходит так, потому что сервис застрял в цикле между строками 230 и 236, в ожидании, пока пройдут установленные 40 секунд. Поэтому первое, что мы должны сделать, - исправить данную ситуацию. Однако, чтобы не тратить время на решение этой ситуации по отдельности, а затем на реализацию функциональности для отображения оставшегося времени бара, мы сделаем и то, и другое за один раз. Новый код для файла C_Replay.mqh в полном объеме можно увидеть тут:

001. //+------------------------------------------------------------------+
002. #property copyright "Daniel Jose"
003. //+------------------------------------------------------------------+
004. #include "C_ConfigService.mqh"
005. #include "C_Controls.mqh"
006. //+------------------------------------------------------------------+
007. #define def_IndicatorControl   "Indicators\\Market Replay.ex5"
008. #resource "\\" + def_IndicatorControl
009. //+------------------------------------------------------------------+
010. #define def_CheckLoopService ((!_StopFlag) && (ChartSymbol(m_Infos.IdReplay) != ""))
011. //+------------------------------------------------------------------+
012. #define def_ShortNameIndControl   "Market Replay Control"
013. #define def_MaxSlider             (def_MaxPosSlider + 1)
014. //+------------------------------------------------------------------+
015. class C_Replay : public C_ConfigService
016. {
017.    private   :
018.       struct st00
019.       {
020.          C_Controls::eObjectControl Mode;
021.          uCast_Double               Memory;
022.          ushort                     Position;
023.          int                        Handle;
024.       }m_IndControl;
025.       struct st01
026.       {
027.          long     IdReplay;
028.          int      CountReplay;
029.          double   PointsPerTick;
030.          MqlTick  tick[1];
031.          MqlRates Rate[1];
032.       }m_Infos;
033.       stInfoTicks m_MemoryData;
034. //+------------------------------------------------------------------+
035. inline bool MsgError(string sz0) { Print(sz0); return false; }
036. //+------------------------------------------------------------------+
037. inline void UpdateIndicatorControl(void)
038.          {
039.             double Buff[];
040.                                  
041.             if (m_IndControl.Handle == INVALID_HANDLE) return;
042.             if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position)
043.             {
044.                if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1)
045.                   m_IndControl.Memory.dValue = Buff[0];
046.                if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay)
047.                   m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition];
048.             }else
049.             {
050.                m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position;
051.                m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode;
052.                m_IndControl.Memory._8b[7] = 'D';
053.                m_IndControl.Memory._8b[6] = 'M';
054.                EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, "");
055.             }
056.          }
057. //+------------------------------------------------------------------+
058.       void SweepAndCloseChart(void)
059.          {
060.             long id;
061.             
062.             if ((id = ChartFirst()) > 0) do
063.             {
064.                if (ChartSymbol(id) == def_SymbolReplay)
065.                   ChartClose(id);
066.             }while ((id = ChartNext(id)) > 0);
067.          }
068. //+------------------------------------------------------------------+
069. inline int RateUpdate(bool bCheck)
070.          {
071.             static int st_Spread = 0;
072. 
073.             st_Spread = (bCheck ? (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time) : st_Spread + 1);
074.             m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread);
075.             CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate);
076.             
077.             return 0;
078.          }
079. //+------------------------------------------------------------------+
080. inline void CreateBarInReplay(bool bViewTick)
081.          {
082.             bool    bNew;
083.             double dSpread;
084.             int    iRand = rand();
085.             static int st_Spread = 0;
086. 
087.             if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew))
088.             {
089.                m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay];
090.                if (m_MemoryData.ModePlot == PRICE_EXCHANGE)
091.                {                  
092.                   dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
093.                   if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
094.                   {
095.                      m_Infos.tick[0].ask = m_Infos.tick[0].last;
096.                      m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
097.                   }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
098.                   {
099.                      m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
100.                      m_Infos.tick[0].bid = m_Infos.tick[0].last;
101.                   }
102.                }
103.                if (bViewTick)
104.                   CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
105.                RateUpdate(true);
106.                st_Spread = (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time);
107.             }
108.             m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread);
109.             CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate);
110.             m_Infos.CountReplay++;
111.          }
112. //+------------------------------------------------------------------+
113.       void AdjustViewDetails(void)
114.          {
115.             MqlRates rate[1];
116. 
117.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
118.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
119.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE);
120.             m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
121.             CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate);
122.             if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE))
123.                for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++);
124.             if (rate[0].close > 0)
125.             {
126.                if (GetInfoTicks().ModePlot == PRICE_EXCHANGE)
127.                   m_Infos.tick[0].last = rate[0].close;
128.                else
129.                {
130.                   m_Infos.tick[0].bid = rate[0].close;
131.                   m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick);
132.                }               
133.                m_Infos.tick[0].time = rate[0].time;
134.                m_Infos.tick[0].time_msc = rate[0].time * 1000;
135.             }else
136.                m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay];
137.             CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
138.          }
139. //+------------------------------------------------------------------+
140.       void AdjustPositionToReplay(void)
141.          {
142.             int nPos, nCount;
143.             
144.             if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return;
145.             nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider);
146.             for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread);
147.             if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1);
148.             while ((nPos > m_Infos.CountReplay) && def_CheckLoopService)
149.                CreateBarInReplay(false);
150.          }
151. //+------------------------------------------------------------------+
152.    public   :
153. //+------------------------------------------------------------------+
154.       C_Replay()
155.          :C_ConfigService()
156.          {
157.             Print("************** Market Replay Service **************");
158.             srand(GetTickCount());
159.             SymbolSelect(def_SymbolReplay, false);
160.             CustomSymbolDelete(def_SymbolReplay);
161.             CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay));
162.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0);
163.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0);
164.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0);
165.             CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation");
166.             CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8);
167.             SymbolSelect(def_SymbolReplay, true);
168.             m_Infos.CountReplay = 0;
169.             m_IndControl.Handle = INVALID_HANDLE;
170.             m_IndControl.Mode = C_Controls::ePause;
171.             m_IndControl.Position = 0;
172.             m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState;
173.          }
174. //+------------------------------------------------------------------+
175.       ~C_Replay()
176.          {
177.             SweepAndCloseChart();
178.             IndicatorRelease(m_IndControl.Handle);
179.             SymbolSelect(def_SymbolReplay, false);
180.             CustomSymbolDelete(def_SymbolReplay);
181.             Print("Finished replay service...");
182.          }
183. //+------------------------------------------------------------------+
184.       bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate)
185.          {
186.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0)
187.                return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket.");
188.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0)
189.                return MsgError("Asset configuration is not complete, need to declare the ticket value.");
190.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0)
191.                return MsgError("Asset configuration not complete, need to declare the minimum volume.");
192.             SweepAndCloseChart();
193.             m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1);
194.             if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl"))
195.                Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl");
196.             else
197.                Print("Apply template: ", szNameTemplate, ".tpl");
198. 
199.             return true;
200.          }
201. //+------------------------------------------------------------------+
202.       bool InitBaseControl(const ushort wait = 1000)
203.          {
204.             Print("Waiting for Mouse Indicator...");
205.             Sleep(wait);
206.             while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200);
207.             if (def_CheckLoopService)
208.             {
209.                AdjustViewDetails();
210.                Print("Waiting for Control Indicator...");
211.                if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false;
212.                ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle);
213.                UpdateIndicatorControl();
214.             }
215.             
216.             return def_CheckLoopService;
217.          }
218. //+------------------------------------------------------------------+
219.       bool LoopEventOnTime(void)
220.          {         
221.             int iPos, iCycles;
222. 
223.             while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay))
224.             {
225.                UpdateIndicatorControl();
226.                Sleep(200);
227.             }
228.             m_MemoryData = GetInfoTicks();
229.             AdjustPositionToReplay();
230.             iPos = iCycles = 0;
231.             while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService))
232.             {
233.                if (m_IndControl.Mode == C_Controls::ePause) return true;
234.                iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0);
235.                CreateBarInReplay(true);
236.                while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause))
237.                {
238.                   Sleep(195);
239.                   iPos -= 200;
240.                   m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks);
241.                   UpdateIndicatorControl();
242.                   iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1);
243.                }
244.             }
245. 
246.             return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService));
247.          }
248. };
249. //+------------------------------------------------------------------+
250. #undef def_SymbolReplay
251. #undef def_CheckLoopService
252. #undef def_MaxSlider
253. //+------------------------------------------------------------------+

Исходный код файла C_Replay.mqh

Далее мы объясним, как работает код. Мы начнем с конца, где сервис вел себя странно, когда ликвидность была очень низкой. Прошу заметить, что код в строке 236 был исправлен. Это позволяет не заблокировать его на длительное время ожидания, что раньше делало его недоступным для пользователя. В этом случае достаточно было добавить проверку того, поставил ли пользователь систему на паузу. Если это произойдет, цикл будет прерван, и, когда выполнение достигнет строки 233, функция закроется и вернется к основному коду. Это, в свою очередь, приведет к новому вызову функции, и она будет переведена в режим ожидания. Однако на этот раз она останется в цикле на строке 223, позволяя пользователю отрегулировать ПИН на индикаторе управления и перейти на более продвинутую позицию. Такое поведение обеспечивает интересный результат, когда символ имеет низкую ликвидность или находится на торгах. Возможно, вам не совсем понятно то, что я описываю, просто рассматривая эту процедуру LoopEventOnTime. Но по мере того, как будут продолжаться объяснения, всё будет становиться понятнее.

Далее рассмотрим изменения, внесенные для сообщения об оставшемся времени бара при низкой ликвидности. Новая переменная добавилась в строке 221 и инициализировалась в строке 230. Теперь обратите внимание на строку 242, где мы используем эту же переменную. Прошу заметить, что мы считаем от нуля до четырех и, когда достигаем значения четыре, вызываем RateUpdate. Но что такое RateUpdate? Не волнуетесь, мы скоро подойдем к этому. Пока отметим, что вызов осуществляется с фиктивным аргументом, а возврат функции присваивается переменной. Это очень важно, поэтому обратите внимание на эту деталь. Помните, я уже говорил, что в одной секунде будет примерно пять циклов? Именно поэтому у нас есть данный счетчик. Идея заключается в том, чтобы заставить указатель мыши получать значение, указывающее на то, что прошла одна секунда. Но это не совсем секунда, а приблизительное время. Причина в том, что мы не измеряем с точностью одну секунду. Ведь цель не в точности, а в том, чтобы дать пользователю приблизительное представление о том, сколько времени осталось до закрытия бара.

Давайте перейдем к другой части того же кода. Перейдем к процедуре, которая начинается со строки 80 и заметим, что из кода удалены строки. В коде снова появляется вызов RateUpdate. Но заметьте, что на этот раз аргумент о прошлом является true. Если мы добавляем новый тик, то аргумент должен быть равен true. А если мы обновляем только время, аргумент будет равен false. Интересно. Теперь давайте рассмотрим процедуру RateUpdate, которая начинается со строки 69 и далее.

Функцию RateUpdate пришлось создать, потому что при обновлении времени был риск пропустить несколько тиков. Это связано со строкой 110. Поскольку мы не хотим рисковать, лучше выполнить данную операцию в другом месте, что и привело к созданию этой функции. Обратите внимание, что переменную, объявленную в строке 85, перенесли в строку 71. Кроме того, работа, которая выполнялась в строках 108 и 109, теперь выполняется в строках 74 и 75, то есть данная функция является практически копией уже существующей. Разница находится в строке 73, но обратите внимание, что данная функция всегда возвращает ноль. Это связано с тем, что строка 242 ожидает этого значения.

Но давайте вернемся к вопросу о строке 73. То, что делает эта строка, довольно интересно. Причина в том, что время не должно быть абсолютно точным, но должно быть максимально точным, насколько это возможно. Когда на графике публикуется тик, вызывается функция CreateBarInReplay. В этом случае значением st_Spread будет время, указанное в тике. Теперь при вызове из LoopEventOnTime значение st_Spread просто увеличится на единицу, что эквивалентно шагу в одну секунду. Независимо от значения st_Spread, как только появится новый тик, значение сбросится до ближайшего фактического значения. Если ликвидность низкая в течение длительного времени, например, 50 секунд, таймер может немного спешить или задержаться, и мы заметим, что указатель мыши будет показывать одно значение, а затем немного другое, с разницей более чем в одну секунду. Это не следует считать сбоем. На самом деле, есть небольшое преимущество: если ликвидность низкая в течение нескольких секунд, то можно будет приостановить и немедленно возобновить сервис. В результате сервис будет игнорировать всё время, которое он должен был бы ждать. Интересно, не правда ли?


Заключительные идеи

Чтобы получить более четкое представление о том, что мы только объяснили, можно использовать моделирование и репликацию тиков символа с низкой ликвидностью. В любом случае, в представленном ниже видео можно увидеть описание того, как приостановить и в последствии возобновить работы, чтобы не ждать несколько секунд.

Однако в рамках данной темы предстоит решить еще одну проблему: как заставить указатель мыши сообщить нам, что символ, возможно, вошел на торги. Это довольно сложный вопрос, чтобы объяснить его, потребуется целая статья. Решение уже реализовано в указателе мыши. Я говорю так, потому что, если разместить его на графике и следить за символом, данные которого поступают с реального торгового сервера, то будет заметно, что при входе символа на торги, указатель мыши сообщает об этом. Однако в симуляторе репликации, где мы используем пользовательский символ, возникает сложная проблема. Но об этом мы расскажем в следующей статье. До скорой встречи!


Демонстрационное видео

Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/12317

Прикрепленные файлы |
Anexo.zip (420.65 KB)
Переосмысливаем классические стратегии (Часть VII): Анализ валютных рынков и суверенного долга на USDJPY Переосмысливаем классические стратегии (Часть VII): Анализ валютных рынков и суверенного долга на USDJPY
Мы проанализируем взаимосвязь между валютными курсами и государственными облигациями. Облигации являются одной из самых популярных форм ценных бумаг с фиксированным доходом. Посмотрим, можно ли улучшить классическую стратегию с помощью ИИ.
Арбитражный трейдинг Forex: Панель оценки взаимосвязей Арбитражный трейдинг Forex: Панель оценки взаимосвязей
Рассмотрим создание арбитражной панели на языке MQl5. Как получать справедливые курсы валют на Forex разными способами? Создадим индикатор для получения отклонений рыночных цен от справедливых курсов, а также для оценки выгоды от арбитражных путей обмена одной валюты на другую (как в треугольном арбитраже).
От начального до среднего уровня: Массив (I) От начального до среднего уровня: Массив (I)
Данная статья является переходом между тем, что рассматривалось до этого, и новым этапом исследований. Чтобы понять эту статью, необходимо прочитать предыдущие. Представленные здесь материалы предназначены только для обучения. Ни в коем случае нельзя рассматривать это приложение как окончательное, цели которого будут иные, кроме изучения представленных концепций.
Нейросети в трейдинге: Двойная кластеризация временных рядов (Окончание) Нейросети в трейдинге: Двойная кластеризация временных рядов (Окончание)
Продолжаем реализацию подходов, предложенных авторами фреймворка DUET, который предлагает инновационный подход к анализу временных рядов, сочетая временную и канальную кластеризацию для выявления скрытых закономерностей в анализируемых данных.