
Разработка системы репликации (Часть 65): Нажатие кнопки воспроизведения в сервисе (VI)
Введение
В предыдущей статье "Разработка системы репликации (часть 64): Нажатие кнопки воспроизведения в сервисе (V)", мы исправили две ошибки в приложении репликации/моделирования. Однако мы решили не все проблемы. Поэтому мы не могли продвигаться дальше к новым темам до написания этой статьи. Нам предстоит решить некоторые мелкие проблемы, которые всё еще остаются нерешенными. При использовании глобальных переменных терминала таких проблем больше не возникало. Однако, поскольку мы перестали их использовать и выбрали другие методы и средства для обеспечения работы приложения репликации/моделирования, нам пришлось адаптировать и создать новую реализацию. Но, как вы наверняка заметили, мы начали не с нуля. Фактически, мы адаптируем и исправляем код, чтобы работа, проделанная при использовании глобальных переменных терминала, не пропала напрасно.
Таким образом, мы почти достигли прежнего уровня функциональности. Однако, чтобы выйти на тот же уровень, нам нужно решить еще несколько деталей. Сегодня мы как раз постараемся всё это сделать, поскольку данные вопросы относительно просты. Это отличается от исправления ошибки с дампом памяти, где проблема была довольно сложной и требовалось основательно разобраться. Поэтому я объяснил, почему произошел сбой, даже если код казался абсолютно правильным. Если бы вместо объяснения я просто показал строку, которую нужно добавить, многие были бы озадачены. Другим вариантом было бы исправить код без объяснений. Но это стало бы очень досадно, поскольку до этого вы никогда не сталкивались с этими проблемами и испытывали бы ложное чувство уверенности в том, что их не существует. А когда они случались бы, вы бы расстраивались, потому что вам некого спросить, или думали бы, что вы не самый лучший специалист. Я не хочу, чтобы кто-то так думал. Даже самые опытные профессионалы допускают ошибки. Даже если мы их не видим, ошибки случаются. Но профессионал быстро обнаруживает и исправляет их. Поэтому я желаю, чтобы все будущие программисты стали настоящими профессионалами. И не просто профессионалами, но и превосходными специалистами в своей области. Давайте теперь начнем с решения первой проблемы.
Добавляем функцию быстрой перемотки вперед (базовая модель)
Данная функция уже существовала в прошлом и была реализована на этапе, когда использовались глобальные переменные терминала. Но поскольку мы больше не используем данные переменные, нам нужно адаптировать код, чтобы перемотка работала правильно. Я буду придерживаться той же философии быстрой перемотки, что и раньше. Так вам будет проще понять, как адаптировать старый код, чтобы новая реализация могла воспользоваться данной функциональностью.
Для начала нам нужно внести небольшую модификацию в код из предыдущей статьи. Цель данной модификации - обеспечить правильную работу индикатора управления. Можно увидеть это в следующем фрагменте:
35. //+------------------------------------------------------------------+ 36. inline void UpdateIndicatorControl(void) 37. { 38. double Buff[]; 39. 40. if (m_IndControl.Handle == INVALID_HANDLE) return; 41. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 42. { 43. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 44. m_IndControl.Memory.dValue = Buff[0]; 45. if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState) 46. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 47. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 48. }else if (m_IndControl.Mode == C_Controls::ePause) 49. { 50. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 51. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 52. m_IndControl.Memory._8b[7] = 'D'; 53. m_IndControl.Memory._8b[6] = 'M'; 54. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 55. } 56. } 57. //+------------------------------------------------------------------+
Фрагмент кода из файла C_Replay.mqh
Изменение или, точнее говоря, добавление, было сделано именно в строке 48. И почему это было необходимо? Причина кроется в коде функции LoopEventOnTime. Теперь, конечно, это кажется запутанным. Зачем вносить изменения в процедуру UpdateIndicatorControl, если причина изменений кроется внутри функции LoopEventOnTime? Это бессмысленно. На самом деле, это было бы бессмысленным, если бы не тот факт, что функция LoopEventOnTime считывает и записывает сообщения для доступа и изменения управляющего флага. Однако, если проверка в строке 48 не существует, то при попытке перейти вперед во времени и затем воспроизвести, то произойдет нечто странное. Это происходит до выполнения процедуры перемотки.
Если мы передвинемся вперед во времени и начнем воспроизводить, то обнаружим, что отправить команду паузы в сервис невозможно. Что это за безумие? Просто нажмите кнопку паузы, и сервис получит обновление. Да, действительно, система приостанавливает процесс, но это не будет иметь немедленного эффекта. Как вы думаете, почему? Из-за строки 41. Проблема в том, что буфер индикатора управления будет находиться в одном месте, а буферная память - в другом. Так что, если мы запустим сервис, затем нажмем кнопку воспроизведения, а потом попытаемся переместиться во времени вперед, то ничего страшного не произойдет. Но если мы приостановим сервис, попытаемся переместиться во времени вперед, а в строке 48 не будет проверки, то панель блокировки индикатора управления будет следовать за прокруткой, которую мы делаем. Это не позволит пользователю вручную регулировать положение.
Если мы запустим сервис репликации/моделирования, передвинемся на одну позицию, а затем нажмем "воспроизвести", мы не сможем остановить сервис с помощью кнопки паузы. Он действительно остановится только тогда, когда проверка в строке 41 будет истинной и индикатор покажет, что выбран режим паузы. А это может занять некоторое время. Объяснение может показаться немного запутанным, но необходимо понимать, что существует три различные ситуации, которые необходимо решить. Каждая из них имеет свои проблемы, поскольку функция LoopEventOnTime постоянно считывает и отправляет сообщения на индикатор управления в течение обычного времени выполнения приложения репликации/моделирования.
Однако, добавив простую проверку, реализованную в строке 48 (как показано в фрагменте), всё решается, и устраняются проблемы, связанные с функцией LoopEventOnTime. Таким образом, мы можем сосредоточиться на реализации быстрой перемотки.
Реализация быстрой перемотки задача несложная. На самом деле это можно реализовать, просто добавив следующие фрагменты кода в файл 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. //+------------------------------------------------------------------+ 014. class C_Replay : public C_ConfigService 015. { 016. private : ... 035. //+------------------------------------------------------------------+ 036. inline void UpdateIndicatorControl(void) 037. { 038. double Buff[]; 039. 040. if (m_IndControl.Handle == INVALID_HANDLE) return; 041. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 042. { 043. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 044. m_IndControl.Memory.dValue = Buff[0]; 045. if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState) 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 if (m_IndControl.Mode == C_Controls::ePause) 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. //+------------------------------------------------------------------+ ... 068. //+------------------------------------------------------------------+ 069. inline void CreateBarInReplay(bool bViewTick) 070. { 071. bool bNew; 072. double dSpread; 073. int iRand = rand(); 074. 075. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 076. { 077. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 078. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 079. { 080. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 081. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 082. { 083. m_Infos.tick[0].ask = m_Infos.tick[0].last; 084. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 085. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 086. { 087. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 088. m_Infos.tick[0].bid = m_Infos.tick[0].last; 089. } 090. } 091. if (bViewTick) CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 092. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 093. } 094. m_Infos.CountReplay++; 095. } 096. //+------------------------------------------------------------------+ ... 123. //+------------------------------------------------------------------+ 124. void AdjustPositionToReplay(void) 125. { 126. int nPos; 127. 128. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxPosSlider * 1.0) / m_MemoryData.nTicks)) return; 129. nPos = (int)(m_MemoryData.nTicks * ((m_IndControl.Position * 1.0) / (def_MaxPosSlider + 1))); 130. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 131. CreateBarInReplay(false); 132. } 133. //+------------------------------------------------------------------+ 134. public : 135. //+------------------------------------------------------------------+ ... 200. //+------------------------------------------------------------------+ 201. bool LoopEventOnTime(void) 202. { 203. int iPos; 204. 205. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 206. { 207. UpdateIndicatorControl(); 208. Sleep(200); 209. } 210. m_MemoryData = GetInfoTicks(); 211. AdjustPositionToReplay(); 212. iPos = 0; 213. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 214. { 215. if (m_IndControl.Mode == C_Controls::ePause) return true; 216. 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); 217. CreateBarInReplay(true); 218. while ((iPos > 200) && (def_CheckLoopService)) 219. { 220. Sleep(195); 221. iPos -= 200; 222. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxPosSlider) / m_MemoryData.nTicks); 223. UpdateIndicatorControl(); 224. } 225. } 226. 227. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 228. } 229. //+------------------------------------------------------------------+ 230. }; 231. //+------------------------------------------------------------------+ 232. #undef macroRemoveSec 233. #undef def_SymbolReplay 234. #undef def_CheckLoopService 235. //+------------------------------------------------------------------+
Фрагменты кода из класса C_Replay.mqh
Обратите внимание, что в приведенном коде с фрагментами из файла C_Replay.mqh содержатся все необходимые шаги для реализации базовой перемотки вперед. Я подчеркиваю слово "базовой", потому что есть мелкие детали, которые мне нужно будет объяснить. Тем не менее, важно, чтобы мы понимали эту базовую модель, поскольку она является основой для полного внедрения, которое мы вскоре предпримем. Обратите внимание, как это отражает то, что объяснялось ранее в строке 48. Примечание строки 207. Именно данная строка вызывает проблемы в индикаторе управления, если бы проверка строки 48 не существовала. Можно проверить наличие проблем, отключив проверку в строке 48, а также в строке 211. Но поскольку в индикаторе управления всё уже работает, мы оставим всё как есть. Теперь мы рассмотрим, как достигается быстрая перемотка в данной базовой версии.
Когда служебный код вызывает функцию LoopEventOnTime, активируется режим паузы. Таким образом, цикл, который начинается в строке 205 и заканчивается в строке 209, будет выполняться бесконечно, позволяя пользователю регулировать положение на индикаторе управления. Всё в порядке. Когда пользователь нажимает кнопку репликации/моделирования, мы фиксируем данные символа в строке 210 для более быстрого доступа. Затем, в строке 211 мы вызываем процедуру в строке 124, которая перемотает текущую позицию в нужную нам.
В строке 128 мы проверяем, является ли указанная пользователем позиция той, которую мы используем в данный момент. Если это так, то процедура перемотки завершается и выполняется цикл между строками 213 и 225. Но если нужная пользователю позиция находится дальше, нам нужно определить ее местоположение, для чего мы производим небольшой расчет в строке 129. Пока ничего экстраординарного, но в строке 130 происходит нечто интересное, поскольку мы входим в цикл, который выполняет строку 131, и замечаем, что передаем процедуре создания бара ложный параметр. Данная процедура должна выполняться как можно быстрее, пока счетчик положения не сравняется с расчетным показателем перемещения. Однако, как можно легко заметить, вызванная в строке 131 процедура выполнит код между строками 69 и 95. Обратите внимание, что мы передаем процедуре создания бара ложный параметр. По этой причине проверка в строке 91 не позволит выполнить вызов CustomTicksAdd. Это предотвратит отправку тиков на символ. Однако строка 92 будет наносить бары на график по мере их построения, один за другим.
Весь процесс работает великолепно, за исключением двух моментов, которые вызывают значительную задержку при выполнении ускоренной перемотки. Если пользователь запросит достаточно большую перемотку, он сможет наблюдать за созданием баров прямо на графике. Строки 75 и 92, помимо вызова процедуры, ответственны за данную задержку. Однако последний вариант не оказывает существенного влияния, поэтому мы можем не обращать внимания на его «стоимость». Две другие линии, особенно линия 75, вызывают наибольшую задержку.
Вот основной способ сделать это. Но есть гораздо более быстрый способ добиться того же самого. Однако, если мы хотим увидеть, как создаются бары, то самый простой метод - это тот метод, который мы только что реализовали. Если изменить код в файле C_Replay.mqh, добавив в него то, что мы только что рассмотрели, быстрая перемотка уже заработает. Поэтому вам решать, использовать данный базовый метод или более сложный, хотя последний заметно ускоряет выполнение быстрой перемотки. Чтобы разграничить информацию, давайте перейдем к новому разделу.
Добавляем функцию быстрой перемотки (динамичная модель)
Если внимательно посмотреть на код, то можно увидеть, что класс C_FileTicks строит минутные бары во время загрузки тиков. Так зачем нам всё это переделывать? Мы просто будем использовать уже созданные бары, чтобы максимально приблизиться к идеальной точке, а затем, если захотим, будем продвигаться вперед, пока не достигнем точно рассчитанной идеальной точки. Это позволит нам ускорить перемотку, создавая впечатление, что она происходит мгновенно.
Однако не всё так идеально с самого начала. Чтобы добиться нужной нам оперативности, необходимо добавить вид индекса или ссылки, позволяющей осуществлять быстрый поиск. Для этого мы будем использовать часть структуры данных, которая в контексте репликации/моделирования не очень полезна: спред внутри структуры MqlRates. Чтобы понять, какие изменения должны быть внесены, давайте рассмотрим следующий фрагмент:
126. //+------------------------------------------------------------------+ 127. bool BarsToTicks(const string szFileNameCSV, int MaxTickVolume) 128. { 129. C_FileBars *pFileBars; 130. C_Simulation *pSimulator = NULL; 131. int iMem = m_Ticks.nTicks, 132. iRet = -1; 133. MqlRates rate[1]; 134. MqlTick local[]; 135. bool bInit = false; 136. 137. pFileBars = new C_FileBars(szFileNameCSV); 138. ArrayResize(local, def_MaxSizeArray); 139. Print("Converting bars to ticks. Please wait..."); 140. while ((*pFileBars).ReadBar(rate) && (!_StopFlag)) 141. { 142. if (!bInit) 143. { 144. m_Ticks.ModePlot = (rate[0].real_volume > 0 ? PRICE_EXCHANGE : PRICE_FOREX); 145. pSimulator = new C_Simulation(SetSymbolInfos()); 146. bInit = true; 147. } 148. ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary); 149. m_Ticks.Rate[++m_Ticks.nRate] = rate[0]; 150. if (pSimulator == NULL) iRet = -1; else iRet = (*pSimulator).Simulation(rate[0], local, MaxTickVolume); 151. if (iRet < 0) break; 152. rate[0].spread = m_Ticks.nTicks; 153. for (int c0 = 0; c0 <= iRet; c0++) 154. { 155. ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray); 156. m_Ticks.Info[m_Ticks.nTicks++] = local[c0]; 157. } 158. m_Ticks.Rate[++m_Ticks.nRate] = rate[0]; 159. } 160. ArrayFree(local); 161. delete pFileBars; 162. delete pSimulator; 163. m_Ticks.bTickReal = false; 164. 165. return ((!_StopFlag) && (iMem != m_Ticks.nTicks) && (iRet > 0)); 166. } 167. //+------------------------------------------------------------------+ 168. datetime LoadTicks(const string szFileNameCSV, const bool ToReplay, const int MaxTickVolume) 169. { 170. int MemNRates, 171. MemNTicks, 172. nDigits, 173. nShift; 174. datetime dtRet = TimeCurrent(); 175. MqlRates RatesLocal[], 176. rate; 177. MqlTick TicksLocal[]; 178. bool bNew; 179. 180. MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate); 181. nShift = MemNTicks = m_Ticks.nTicks; 182. if (!Open(szFileNameCSV)) return 0; 183. if (!ReadAllsTicks()) return 0; 184. rate.time = 0; 185. nDigits = SetSymbolInfos(); 186. m_Ticks.bTickReal = true; 187. for (int c0 = MemNTicks, c1, MemShift = nShift; c0 < m_Ticks.nTicks; c0++, nShift++) 188. { 189. if (nShift != c0) m_Ticks.Info[nShift] = m_Ticks.Info[c0]; 190. if (!BuildBar1Min(c0, rate, bNew)) continue; 191. if (bNew) 192. { 193. if ((m_Ticks.nRate >= 0) && (ToReplay)) if (m_Ticks.Rate[m_Ticks.nRate].tick_volume > MaxTickVolume) 194. { 195. nShift = MemShift; 196. ArrayResize(TicksLocal, def_MaxSizeArray); 197. C_Simulation *pSimulator = new C_Simulation(nDigits); 198. if ((c1 = (*pSimulator).Simulation(m_Ticks.Rate[m_Ticks.nRate], TicksLocal, MaxTickVolume)) > 0) 199. nShift += ArrayCopy(m_Ticks.Info, TicksLocal, nShift, 0, c1); 200. delete pSimulator; 201. ArrayFree(TicksLocal); 202. if (c1 < 0) return 0; 203. } 204. rate.spread = MemShift; 205. MemShift = nShift; 206. ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary); 207. }; 208. m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate; 209. } 210. if (!ToReplay) 211. { 212. ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates)); 213. ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0); 214. CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates)); 215. dtRet = m_Ticks.Rate[m_Ticks.nRate].time; 216. m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates); 217. m_Ticks.nTicks = MemNTicks; 218. ArrayFree(RatesLocal); 219. }else m_Ticks.nTicks = nShift; 220. 221. return dtRet; 222. }; 223. //+------------------------------------------------------------------+
Фрагмент кода из файла C_FileTicks.mqh
Данный фрагмент показывает, какие именно строки нужно изменить в файле C_FileTicks.mqh. Обратите внимание, что строку 149 нужно удалить и перенести на новую позицию, а именно на строку 158. Зачем это делать? Будьте спокойны, всё будет объяснено в свое время. Теперь посмотрите на то, что была добавлена новая строка 152. Обратите внимание на следующий факт: функция здесь преобразует бары в тики с помощью моделирования. Поэтому в строке 152 хранится значение индекса, с которого начинается новый бар. Данное значение сохраняется в спреде бара.
Теперь перейдем к следующей функции, где мы сделаем что-то очень похожее. Обратите внимание, что добавилась только строка 204, в которой будут считываться тики. Но помните следующее: даже если мы считываем тики из файла, может возникнуть необходимость сбросить их и смоделировать те, которые будут использоваться вместо них. Мы видели это в предыдущих статьях, где объяснялись причины, побудившие меня сделать нечто подобное. Поэтому значение, которое нас действительно интересует, - это значение памяти, т.е. MemShift, которое указывает нам, где начинается новый бар. Как и в предыдущей функции, здесь мы сохраняем данное значение в спреде структуры rates.
Хорошо, но зачем мы это делаем? Какова реальная польза? Давайте разберемся кое в чем. Во время моделирования баров в тиках, которое выполняется в предыдущей функции, мы точно знаем, когда и где начнется каждый бар. Это возможно, потому что есть индекс, который указывает именно на это начальное положение бара перед его моделированием. Таким же образом в функции, которая загружает тики из файла, мы преобразуем тики в бар в строке 190, как это было бы сделано в классе C_Replay. Поэтому мы точно знаем, где будет начинаться каждый новый бар, созданный здесь. По этой причине нам не нужно возлагать эту задачу на класс C_Replay, который должен был бы найти начальную точку бара. И поскольку значение спреда в данный момент не имеет никакой пользы для приложения репликации/моделирования, мы используем данное значение, которое в принципе кажется бесполезным, для хранения чего-то ценного для нас: точной точки начала каждого бара.
Если посмотреть на процедуру ускоренной перемотки, описанную в предыдущей теме, то мы увидим, что в строке 129 выполняется вычисление. Этот расчет позволяет с точностью определить, к какому индексу следует перейти, чтобы быстро пройти репликацию или моделирование. Вам становится понятно важность этого значения, которое генерируется во время загрузки тиков? Данное значение очень важно и позволит вам быстрее продвигаться вперед. И это связано с тем, что не нужно будет перестраивать бар за баром. Мы можем перейти непосредственно к нужной точке, а затем попросить MetaTrader 5 отобразить и обновить бары между двумя точками. Другими словами, теперь мы можем действовать другим способом.
Чтобы использовать эти данные с пользой, нам нужно внести некоторые изменения в код класса C_Replay. Однако данные изменения будут минимальными по сравнению с тем, что обсуждалось в предыдущей теме. Поэтому в файл C_Replay.mqh необходимо добавить следующие фрагменты, которые можно увидеть ниже.
013. #define def_MaxSlider (def_MaxPosSlider + 1) ... 124. //+------------------------------------------------------------------+ 125. void AdjustPositionToReplay(void) 126. { 127. int nPos, nCount; 128. 129. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 130. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 131. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 132. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 133. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 134. CreateBarInReplay(false); 135. } 136. //+------------------------------------------------------------------+
Фрагмент кода из файла C_Replay.mqh
Как видите, нам пришлось добавить новую строку в заголовочный файл C_Replay.mqh. Это строка 13, где мы вносим небольшое исправление в общий расчет перемещения. Как всегда, для всего есть своя причина. Если не проводить данную настройку, появится другая проблема или нам придется действовать по-другому. Чтобы не переделывать большую часть моделирования данных, я предпочитаю просто настроить в этом месте. Почему так важна эта корректировка в строке 13? Причина в том, что без него мы окажемся «беззащитными» перед скроллингом, где последнее положение ползунка индикатора управления приведет к завершению текущего моделирования или репликации. Простое добавление позиции здесь позволяет нам получить несколько дополнительных тиков, которые можно применить на графике. Это очень практично и обеспечивает упорядоченное завершение работы моделирования или репликации.
Однако то, что действительно важно для нас, находится в строках 131 и 132. Как видите, просто добавив эти две строки, мы сможем продвигаться гораздо быстрее, чем раньше. Однако некоторые тики всё же могут оказаться не в нужном положении, и их придется добавить, как это делалось раньше. Данные тики будут использовать цикл, присутствующий в строке 133, но поскольку в большинстве случаев их будет очень мало, процесс станет довольно быстрым.
В любом случае, мы делаем следующее: в строке 131 мы ищем индекс бара, значение которого находится непосредственно под точкой, где мы хотим находиться. Это делается полностью в цикле for. Хотя данная конструкция может показаться кому-то нестандартной, она вполне функциональна. Ее нестандартность объясняется тем, что мы включаем в объявление цикла присвоение значения, которое должно быть помещено в счетчик репликации CountReplay. Однако нет никаких причин, по которым данное присвоение не может быть выполнено вне цикла.
В строке 132 нам нужно проверить значение nCount. Я не хочу рисковать тем, чтобы вызов библиотеки MQL5 CustomRatesUpdate неправильно интерпретировал данные или бары, которые будут использоваться. Мы уже описали остальные функции в предыдущей теме. Эти изменения интересны тем, что из-за них окончательный код файла 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. 075. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 076. { 077. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 078. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 079. { 080. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 081. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 082. { 083. m_Infos.tick[0].ask = m_Infos.tick[0].last; 084. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 085. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 086. { 087. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 088. m_Infos.tick[0].bid = m_Infos.tick[0].last; 089. } 090. } 091. if (bViewTick) CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 092. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 093. } 094. m_Infos.CountReplay++; 095. } 096. //+------------------------------------------------------------------+ 097. void AdjustViewDetails(void) 098. { 099. MqlRates rate[1]; 100. 101. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 102. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 103. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); 104. m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); 105. CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate); 106. if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) 107. for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++); 108. if (rate[0].close > 0) 109. { 110. if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) 111. m_Infos.tick[0].last = rate[0].close; 112. else 113. { 114. m_Infos.tick[0].bid = rate[0].close; 115. m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick); 116. } 117. m_Infos.tick[0].time = rate[0].time; 118. m_Infos.tick[0].time_msc = rate[0].time * 1000; 119. }else 120. m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay]; 121. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 122. } 123. //+------------------------------------------------------------------+ 124. void AdjustPositionToReplay(void) 125. { 126. int nPos, nCount; 127. 128. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 129. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 130. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 131. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 132. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 133. CreateBarInReplay(false); 134. } 135. //+------------------------------------------------------------------+ 136. public : 137. //+------------------------------------------------------------------+ 138. C_Replay() 139. :C_ConfigService() 140. { 141. Print("************** Market Replay Service **************"); 142. srand(GetTickCount()); 143. SymbolSelect(def_SymbolReplay, false); 144. CustomSymbolDelete(def_SymbolReplay); 145. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 146. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 147. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 148. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 149. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 150. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 151. SymbolSelect(def_SymbolReplay, true); 152. m_Infos.CountReplay = 0; 153. m_IndControl.Handle = INVALID_HANDLE; 154. m_IndControl.Mode = C_Controls::ePause; 155. m_IndControl.Position = 0; 156. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 157. } 158. //+------------------------------------------------------------------+ 159. ~C_Replay() 160. { 161. SweepAndCloseChart(); 162. IndicatorRelease(m_IndControl.Handle); 163. SymbolSelect(def_SymbolReplay, false); 164. CustomSymbolDelete(def_SymbolReplay); 165. Print("Finished replay service..."); 166. } 167. //+------------------------------------------------------------------+ 168. bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate) 169. { 170. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0) 171. return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); 172. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0) 173. return MsgError("Asset configuration is not complete, need to declare the ticket value."); 174. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0) 175. return MsgError("Asset configuration not complete, need to declare the minimum volume."); 176. SweepAndCloseChart(); 177. m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); 178. if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) 179. Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl"); 180. else 181. Print("Apply template: ", szNameTemplate, ".tpl"); 182. 183. return true; 184. } 185. //+------------------------------------------------------------------+ 186. bool InitBaseControl(const ushort wait = 1000) 187. { 188. Print("Waiting for Mouse Indicator..."); 189. Sleep(wait); 190. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 191. if (def_CheckLoopService) 192. { 193. AdjustViewDetails(); 194. Print("Waiting for Control Indicator..."); 195. if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 196. ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle); 197. UpdateIndicatorControl(); 198. } 199. 200. return def_CheckLoopService; 201. } 202. //+------------------------------------------------------------------+ 203. bool LoopEventOnTime(void) 204. { 205. int iPos; 206. 207. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 208. { 209. UpdateIndicatorControl(); 210. Sleep(200); 211. } 212. m_MemoryData = GetInfoTicks(); 213. AdjustPositionToReplay(); 214. iPos = 0; 215. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 216. { 217. if (m_IndControl.Mode == C_Controls::ePause) return true; 218. 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); 219. CreateBarInReplay(true); 220. while ((iPos > 200) && (def_CheckLoopService)) 221. { 222. Sleep(195); 223. iPos -= 200; 224. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 225. UpdateIndicatorControl(); 226. } 227. } 228. 229. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 230. } 231. }; 232. //+------------------------------------------------------------------+ 233. #undef macroRemoveSec 234. #undef def_SymbolReplay 235. #undef def_CheckLoopService 236. #undef def_MaxSlider 237. //+------------------------------------------------------------------+
Окончательный исходный код файла C_Replay.mqh
Обновляем время работы бара и процент котировки
Эту проблему можно решить довольно просто. Необходимо только отправить сообщения индикатору мыши, чтобы он мог интерпретировать и представить эту информацию соответствующим образом. Теперь проблема заключается лишь в том, чтобы заставить индикатор сообщать нам, сколько времени осталось до начала нового бара и каково процентное изменение между закрытием предыдущего дня и текущей ценой.
Для большей простоты мы сначала настроим процентное соотношение. При желании можно создать свой собственный метод на основе того, что мы покажем здесь. Не стесняйтесь делать это по своему усмотрению. Давайте теперь начнем разбираться в проблеме с процентным изменением. Если посмотреть на индикатор положения мыши, то вы увидите, что процентное изменение между закрытием и текущей ценой не показывает правильных значений. Однако значение, основанное на положении мыши, будет правильным. Почему существует такая разница? Можно подумать, что проблема связана с тем, что указатель мыши не может определить, где заканчиваются предыдущие данные и начинается моделирование или репликация. Но это не совсем то, что происходит. Проблема заключается в другом: указатель мыши может считывать и интерпретировать данные, о чем свидетельствуют колебания, отображаемые при его перемещении. Однако происходит нечто, что сбивает индикатор с толку и заставляет его отображать неправильные значения, а время от времени он отображает правильное значение. Это проблема, которую мы должны решить.Чтобы решить её, достаточно будет сделать кое-что очень простое. Однако я хочу предупредить вас, что вам следует по возможности избегать использования того, что мы здесь рассмотрим. Причина в том, что если вы не будете действовать осторожно, то можете потерять контроль над проектом. Давайте посмотрим, как решилась эта проблема.
В первую очередь необходимо изменить код указателя мыши, как показано в следующем фрагменте.09. #property indicator_chart_window 10. #property indicator_plots 0 11. #property indicator_buffers 1 12. //+------------------------------------------------------------------+ 13. double GL_PriceClose; 14. //+------------------------------------------------------------------+ 15. #include <Market Replay\Auxiliar\Study\C_Study.mqh> 16. //+------------------------------------------------------------------+ 17. C_Study *Study = NULL; 18. //+------------------------------------------------------------------+ 19. input color user02 = clrBlack; //Price Line 20. input color user03 = clrPaleGreen; //Positive Study 21. input color user04 = clrLightCoral; //Negative Study 22. //+------------------------------------------------------------------+ 23. C_Study::eStatusMarket m_Status; 24. int m_posBuff = 0; 25. double m_Buff[]; 26. //+------------------------------------------------------------------+ 27. int OnInit() 28. { 29. ResetLastError(); 30. Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04); 31. if (_LastError != ERR_SUCCESS) return INIT_FAILED; 32. if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay) 33. { 34. MarketBookAdd((*Study).GetInfoTerminal().szSymbol); 35. OnBookEvent((*Study).GetInfoTerminal().szSymbol); 36. m_Status = C_Study::eCloseMarket; 37. }else 38. m_Status = C_Study::eInReplay; 39. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 40. ArrayInitialize(m_Buff, EMPTY_VALUE); 41. 42. return INIT_SUCCEEDED; 43. } 44. //+------------------------------------------------------------------+ 45. int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], 46. const double& high[], const double& low[], const double& close[], const long& tick_volume[], 47. const long& volume[], const int& spread[]) 48. //int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 49. { 50. GL_PriceClose = close[rates_total - 1]; 51. m_posBuff = rates_total; 52. (*Study).Update(m_Status); 53. 54. return rates_total; 55. } 56. //+------------------------------------------------------------------+
Фрагмент кода указателя мыши
Обратите внимание, что в строке 13 мы добавили переменную. Данная переменная является глобальной не только потому что она не находится внутри процедуры или функции, но и из-за того, что она глобальна по своему расположению и точке объявления. Эта переменная не выполняет никаких особо сложных действий. Однако в строке 50 мы получим значение, которое будет выдавать MetaTrader 5. Обратите внимание, что строку 48, которая соответствовала предыдущему объявлению функции OnCalculate, заменили на другую. Это очень важно. Теперь мы можем использовать данную переменную, объявленную в строке 13, для решения задачи о процентных соотношениях. Следующее изменение должно быть сделано в заголовочном файле C_Study.mqh. Это можно увидеть ниже:
41. //+------------------------------------------------------------------+ 42. void Draw(void) 43. { 44. double v1; 45. 46. if (m_Info.bvT) 47. { 48. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 18); 49. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_TEXT, m_Info.szInfo); 50. } 51. if (m_Info.bvD) 52. { 53. v1 = NormalizeDouble((((GetInfoMouse().Position.Price - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 54. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1); 55. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP)); 56. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); 57. } 58. if (m_Info.bvP) 59. { 60. v1 = NormalizeDouble((((GL_PriceClose - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 61. v1 = NormalizeDouble((((iClose(GetInfoTerminal().szSymbol, PERIOD_D1, 0) - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 62. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1); 63. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP)); 64. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); 65. } 66. } 67. //+------------------------------------------------------------------+
Фрагмент кода из файла C_Study.mqh
Обратите внимание, что строку 61, которая соответствовала предыдущему коду индикатора, заменили на новый код в строке 60. Если вы заметили, здесь используется переменная, объявленная в коде индикатора. Как это возможно? Это происходит так, потому что мы объявили ее глобальной переменной. Точнее, мы объявили ее в теле кода, чтобы она была доступна из любой точки создаваемого нами кода. Подобная практика часто приводит к проблемам. Поэтому мы должны быть очень осторожны при объявлении переменных в качестве глобальных переменных.
Если нам это нужно сделать по какой-то причине, мы это сделаем, зная, что переменная может вызвать у проблемы и что мы должны быть осторожны с ней. Обычно я определяю ее перед включаемыми файлами и добавляю префикс GL_. Хотя в строке 25 есть еще одна переменная с глобальной областью видимости, я не слишком беспокоюсь об этом, потому что у нее очень специфическое назначение и мне редко придется изменять ее. Однако следует обратить внимание на переменную в строке 13, так как мы можем случайно изменить ее.
Таким простым способом мы решаем проблему с процентными соотношениями, которые иногда отображались некорректно. Кроме того, у нас получилось быстрее выполнить код. Это связано с тем, что нам больше не нужно перехватывать значение закрытия с помощью вызова iClose. MetaTrader 5 сам предоставляет эту информацию, избавляя нас от необходимости искать ее вручную.
Заключение
Хотя мы еще не объяснили, как решить проблему, связанную с временем закрытия бара в случае использования репликации или моделирования, в этой статье мы добились большого прогресса. Приложение стало намного ближе к тому, что было, когда мы использовали глобальные переменные терминала. Однако я думаю, что многие из вас будут удивлены тем, как много вещей, о которых вы не подозревали, можно сделать с помощью простого MQL5. Но это только начало. Многое еще предстоит сделать, и каждая новая задача будет новым вызовом и новым опытом для нас.
Хотя я еще не объяснил, как заставить MetaTrader 5 сообщать нам об оставшемся времени на баре при использовании приложения репликации/моделирования, я расскажу об этом в начале следующей статьи. Не пропустите, потому что мы также начнем настраивать еще один момент, который необходимо улучшить, чтобы он правильно работал с теми элементами, которые мы уже используем.
К сожалению, данная статья пока не позволяет пользователям без навыков программирования использовать приложение. Это связано с тем, что до сих пор не решен вопрос о времени закрытия бара. Но если вы знаете, как программировать, и смогли следовать за объяснениями и вносить изменения, которые мы показали, вы увидите, что приложение для репликации/моделирования будет вести себя так, как показано на видео ниже. До встречи в следующей статье данной серии.
Демонстрационное видео
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/12265
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования