Моделирование рынка: Первые шаги на SQL в MQL5 (V)
Введение
Здравствуйте и добро пожаловать в очередную статью из серии о том, как создать систему репликации/моделирования.
В предыдущей статье Моделирование рынка: Первые шаги на SQL в MQL5 (IV) мы описали небольшую меру предосторожности, которую следует соблюдать при использовании SQLite в MetaTrader 5. Хотя в будущем это может измениться, в целях предосторожности всегда следует сначала проводить тестирование, прежде чем использовать SQL с SQLite в среде MetaTrader 5. В любом случае, там мы показали, как следует действовать, чтобы добавить механизм запросов, так что в коде MQL5 мы могли бы в полной мере использовать SQL и получать результаты с помощью команды SQL SELECT FROM.
Но осталось рассказать о последней функции, которую нам нужно реализовать. Это функция DatabaseReadBind. И, поскольку для правильного понимания требуется чуть более развернутое объяснение, было решено сделать это не в той предыдущей статье, а в сегодняшней. Итак, поскольку эта тема будет довольно объемной, перейдём сразу к следующей.
Реализация функции чтения запросов
Реализация функции, отвечающей за чтение данных, возвращаемых SQL-запросом, довольно проста, но есть одна деталь, которую никогда не следует забывать. Результат, который вернет функция чтения, — это именно результат предыдущего выполнения вызова ExecRequestOfData. Поэтому вызов ExecRequestOfData всегда должен предшествовать вызову чтения возвращённых данных.
Здесь, однако, следует сделать отступление. Чтобы лучше это понять, нам сначала нужно взглянуть на код класса. Таким образом, полный код класса C_DB_SQL приведен ниже. Помните, что этот код ориентирован на использование встроенного SQL в MetaTrader 5. Для других сценариев использования SQL этот код придется изменить, чтобы внедрить необходимые возможности.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "..\Service Graphics\Support\C_Array.mqh" 005. //+------------------------------------------------------------------+ 006. class C_DB_SQL 007. { 008. private : 009. C_Array m_Arr; 010. int m_handleDB, 011. m_Request; 012. //+------------------------------------------------------------------+ 013. void Convert(const char &buff[], const int size) 014. { 015. string sz0 = ""; 016. bool b0, b1, bs1, bs2, bc0, bc1, bc; 017. int nLine = 1; 018. 019. b0 = b1 = bs1 = bs2 = bc0 = bc1 = bc = false; 020. for (int count = 0, nC0 = nLine; count < size; count++) 021. { 022. switch (buff[count]) 023. { 024. case '\t': 025. sz0 += (bs1 || bs2 ? "\t" : ""); 026. break; 027. case '\n': 028. nC0++; 029. case '\r': 030. bc0 = false; 031. break; 032. case ';': 033. b0 = (bs1 || bs2 || bc0 || bc1 ? b0 : true); 034. default: 035. switch (buff[count]) 036. { 037. case '"': 038. bs1 = (bs2 || bc0 || bc1 ? bs1 : !bs1); 039. break; 040. case '\'': 041. bs2 = (bs1 || bc0 || bc1 ? bs2 : !bs2); 042. break; 043. } 044. if (((count + 1) < size) && (!bs1) && (!bs2)) 045. { 046. if (bc = ((buff[count] == '-') && (buff[count + 1] == '-'))) bc0 = true; 047. if (bc = ((buff[count] == '/') && (buff[count + 1] == '*'))) bc1 = true; 048. if (bc = ((buff[count] == '*') && (buff[count + 1] == '/'))) bc1 = false; 049. if (bc) 050. { 051. count += 1; 052. bc = false; 053. continue; 054. } 055. } 056. if (!(bc0 || bc1)) 057. { 058. if ((!b1) && (buff[count] > ' ')) 059. { 060. b1 = true; 061. nLine = nC0; 062. } 063. sz0 += (b1 ? StringFormat("%c", buff[count]) : ""); 064. } 065. } 066. if (b0) 067. { 068. m_Arr.Add(sz0, nLine); 069. sz0 = ""; 070. b0 = b1 = false; 071. } 072. } 073. } 074. //+------------------------------------------------------------------+ 075. const string ExecSQL(void) 076. { 077. string szCmd; 078. 079. for (int count = 0, nLine; count >= 0; count++) 080. { 081. szCmd = m_Arr.At(count, nLine); 082. if (nLine < 0) break; 083. if (!ExecCommandSQL(szCmd)) 084. return StringFormat("Execution of line %d of the SQL script failed...", nLine); 085. } 086. 087. return NULL; 088. } 089. //+------------------------------------------------------------------+ 090. public : 091. //+------------------------------------------------------------------+ 092. C_DB_SQL(const string szFileName = ":memory:") 093. { 094. m_handleDB = DatabaseOpen(szFileName, DATABASE_OPEN_CREATE | DATABASE_OPEN_READWRITE); 095. m_Request = INVALID_HANDLE; 096. } 097. //+------------------------------------------------------------------+ 098. ~C_DB_SQL() 099. { 100. DatabaseClose(m_handleDB); 101. } 102. //+------------------------------------------------------------------+ 103. const string ExecResourceSQL(const string szResource) 104. { 105. char buff[]; 106. int size; 107. 108. ArrayResize(buff, size = StringLen(szResource)); 109. StringToCharArray(szResource, buff); 110. Convert(buff, size); 111. ArrayFree(buff); 112. 113. return ExecSQL(); 114. } 115. //+------------------------------------------------------------------+ 116. const string ExecScriptSQL(const string szFileName) 117. { 118. int file, size; 119. char buff[]; 120. 121. if ((file = FileOpen(szFileName, FILE_READ | FILE_BIN)) == INVALID_HANDLE) 122. return StringFormat("Unable to open script file: %s", szFileName); 123. ArrayResize(buff, size = (int) FileSize(file)); 124. FileReadArray(file, buff); 125. FileClose(file); 126. Convert(buff, size); 127. ArrayFree(buff); 128. 129. return ExecSQL(); 130. } 131. //+------------------------------------------------------------------+ 132. bool ExecCommandSQL(const string szCmd) 133. { 134. return (m_handleDB == INVALID_HANDLE ? false : DatabaseExecute(m_handleDB, szCmd)); 135. } 136. //+------------------------------------------------------------------+ 137. bool ExecRequestOfData(const string szCmd) 138. { 139. if (m_Request != INVALID_HANDLE) DatabaseFinalize(m_Request); 140. return ((m_Request = DatabasePrepare(m_handleDB, szCmd)) != INVALID_HANDLE); 141. } 142. //+------------------------------------------------------------------+ 143. template <typename T> bool GetRegisterOfRequest(T &stObject, const bool Finish = true) 144. { 145. if (!DatabaseReadBind(m_Request, stObject)) 146. { 147. if (Finish) 148. { 149. DatabaseFinalize(m_Request); 150. m_Request = INVALID_HANDLE; 151. } 152. return false; 153. } 154. 155. return true; 156. } 157. //+------------------------------------------------------------------+ 158. }; 159. //+------------------------------------------------------------------+
Код C_DB_SQL.mqh
Итак, это полный код класса. Но, поскольку лишь малая часть ещё не была прокомментирована, или, если быть точнее, не была показана ранее, мы не будем рассматривать функции или моменты, уже описанные в других статьях. Здесь мы сосредоточимся исключительно на функции строки 143. Речь идет о функции GetRegisterOfRequest. Первый и вполне справедливый вопрос: почему данная функция объявляется именно таким образом? Этот момент довольно сложно объяснить, используя только MQL5.
Чтобы детально разобраться в этом, потребовалось бы объяснить одну очень сложную концепцию, существующую в языке C/C++. Эта концепция предполагает использование указателей типа void. Да, и мы можем объявлять и использовать указатели типа void, а не только обычные типы, как многие себе представляют. Но поскольку объяснить эту концепцию в статье крайне сложно, мы постараемся показать вам, уважаемый читатель, почему эта функция объявлена именно таким образом, избегая, насколько это возможно, описания языка C/C++.
Начнем с того, что здесь мы имеем дело с зависимостью типов. Не в объявлении функции, которое делается в строке 143, а в функции DatabaseReadBind. Эта зависимость вынуждает нас использовать определенный тип данных, чтобы компилятор MQL5 не выдавал лавину ошибок при попытке скомпилировать код. Многие программисты, когда собираются использовать функцию DatabaseReadBind или пытаются сделать это, не помещают её в вызов, подобный тому, который мы делаем здесь. Причина кроется именно в том, что они не понимают возникающую здесь зависимость от типа.
Итак, что они обычно делают? Они создают какую-то процедуру или функцию, где выполняется запрос, анализируется возвращенный SQL результат и, наконец, значение возвращается вызывающему объекту обычным образом. Итак, такой подход работает и вполне подходит для очень специфических случаев. В других случаях программисты просто используют функцию DatabaseReadBind в основном коде, когда она им нужна.
Но почему они прибегают к такому подходу? Опять же, причина в зависимости от типа. А вот в MQL5 с этим справиться не так-то просто. Однако при использовании C/C++ ситуация немного меняется, так как в этом случае мы можем использовать указатель типа void. Думаю, вы пока ещё не до конца разобрались в этом вопросе. Итак, давайте посмотрим, как объявить функцию DatabaseReadBind в MQL5. Можно увидеть это ниже:
bool DatabaseReadBind( int request, // manipulador da consulta criada no DatabasePrepare void& struct_object // referência para a estrutura para leitura do registro );
То, что мы видим, можно найти в документации к функциям MQL5. Возможно, вам это не совсем понятно, но для программиста на C/C++ такое объявление имеет полный смысл. Не вдаваясь в подробности о C/C++, суть в следующем: функция должна принимать два параметра. Первый параметр — это целочисленное значение, представляющее собой дескриптор, возвращаемый функцией DatabasePrepare. Мы уже говорили об этом в предыдущей статье. Однако здесь ключевую роль играет второй параметр. Второй параметр имеет тип void, то есть функция принимает ссылку на объект без явного указания конкретного типа в сигнатуре.
Однако здесь передаётся не адрес в привычном смысле, а ссылка на объект в памяти. Это связано с тем, что указатели обычно ссылаются на адрес в памяти. Однако, когда мы используем символ амперсанда (&) перед именем, мы указываем, что будем ссылаться на переменную, а не на позицию. Если вы действительно хотите в этом разобраться, постарайтесь научиться использовать указатели в C/C++, так как данная тема зачастую требует нескольких глав в книге, ориентированной на обучение C/C++. Эта тема довольно сложная и запутанная.
Поскольку MQL5 не позволяет нам использовать указатели так же, как в C/C++, нам нужен небольшой трюк, чтобы получить доступ к функции DatabaseReadBind, как это было показано в коде класса C_DB_SQL. Этот трюк заключается как раз в использовании системы приведения типов, при которой мы сообщаем функции, что она сможет принимать любой тип, и компилятор внутренне преобразует его в подходящий тип. На первый взгляд, это кажется чем-то очень сложным и трудноразрешимым. Но, к счастью, MQL5 предоставляет нам возможность использовать то, что также существует в C/C++, а именно объявление:
template <typename T >
Здесь есть один нюанс. T может быть чем угодно, но обычно в программировании мы используем Т по соглашению. Нюанс заключается в том, что именно это объявление, стоящее перед именем функции, указывает компилятору на необходимость выполнить правильное приведение типов для решения проблемы типизации. Теперь обратите внимание на следующее: первым параметром функции GetRegisterOfRequest является именно T. Поскольку мы не можем использовать тип void, данное объявление, таким образом, заменяет способ его использования в C/C++.
"Хорошо. Но я всё ещё не могу понять эту логику. Изучив документацию MQL5 по функции DatabaseReadBind, мы видим, что в ней указано, что второй параметр должен быть структурой. Даже в примерах, где демонстрируется использование данной функции, в качестве второго параметра также используется структура. Поэтому я не могу понять, почему возникла эта сложность с использованием template <typename T > и это странное объявление в строке 143. Почему бы просто не передать структуру непосредственно в эту функцию? Зачем так усложнять?"
Действительно, уважаемый читатель, я полностью разделяю ваш взгляд на этот счет. Вы правы. Мы могли бы просто передать структуру в качестве параметра функции, и это значительно упростило бы объявление в строке 143. Но теперь я спрошу вас: сколько полей и какого типа поля должна иметь эта структура? Если вы сможете ответить мне на этот вопрос так, чтобы это позволило обобщить логику, я со всей определенностью смоделирую вызов в строке 143 иначе.
Но данная структура никогда не будет построена таким образом, чтобы это позволило обобщить все ситуации. Это связано с тем, что даже если мы знаем количество полей, присутствующих в базе данных, рано или поздно это количество может измениться. И если это произойдёт, всю сборку придётся переделывать здесь, в коде MQL5. По этой причине многие используют функцию DatabaseReadBind способом, отличающимся от показанного здесь.
Эта функция, которую мы показываем, и цель которой — вернуть в наш код на MQL5 то, что вернул SQL, является обобщенной функцией. И вы скоро поймете почему. В любом случае, в строке 145 мы выполним вызов, и, в зависимости от возвращаемого значения, у нас возникнут две совершенно разные ситуации. Первая ситуация возникает тогда, когда функции DatabaseReadBind удается прочитать какую-либо запись, возвращенную SQL. В этом случае выполнится строка 155. Но может случиться так, что в этом блоке больше не останется записей. Тогда у нас будет две разные ситуации. Однако оба варианта останутся в пределах блока ошибок, связанного с возвратом значения функции DatabaseReadBind.
Прошу заметить, что по умолчанию второй параметр функции GetRegisterOfRequest имеет значение true, то есть истинное значение. Когда проверка в строке 145 не удастся, мы проверим в строке 147, является ли значение второго параметра истинным или нет. Если вызывающая сторона не изменила его, в строке 149 вызывается DatabaseFinalize. И, для завершения, в строке 150 мы изменяем значение переменной m_Request, указывая, что доступных записей больше нет и потребуется выполнить новый запрос к базе данных. Однако, если вызывающая сторона укажет, что значение параметра Finish равно false, проверка в строке 147 завершится неудачей, и всё останется как есть. Это пригодится нам позже в основном коде. Но в любом случае, возвращаемое значение будет false, и это связано со строкой 152.
Таким образом, мы полностью завершаем основную часть класса C_DB_SQL. Отныне любой код сможет использовать ее внутри нашей системы воспроизведения и симуляции. Но прежде чем это сделать, нам необходимо убедиться, что всё работает должным образом. И для этого давайте перейдем к новой теме, чтобы мы могли лучше разграничить вопросы.
Тестирование класса C_DB_SQL для использования с SQLite в MetaTrader 5
Большая часть нашего класса C_DB_SQL уже протестирована. Теперь осталось только протестировать часть, касающуюся запросов, и каким-то образом понять, что именно вернет нам SQL при выполнении запроса.
Чтобы попытаться объяснить это должным образом и в то же время сделать так, чтобы это было легко понять каждому, особенно вам, уважаемый читатель. Таким образом, мы покажем фрагмент основного кода, продемонстрируем полученный результат и объясним, что происходит и почему получили именно такой результат. И ближе к концу статьи мы выложим полный код, чтобы вы могли локально протестировать то, что мы здесь сделаем.
После того, как мы разберемся с этой частью, перейдем к первому тесту. Сделаем это с помощью следующего фрагмента:
18. //+------------------------------------------------------------------+ 19. void SELECT_Type_01(void) 20. { 21. struct st 22. { 23. int id; 24. string symbol; 25. }stLocal; 26. 27. Print("Executing type 1 data request..."); 28. if ((*SQL).ExecRequestOfData("SELECT * FROM tb_Symbols")) 29. { 30. Print("Request: OK..."); 31. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++) 32. Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol); 33. Print("Reload..."); 34. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 35. Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol); 36. }; 37. Print("Finish type 1..."); 38. } 39. //+------------------------------------------------------------------+
Фрагмент из основного файла
Нумерация строк в точности соответствует той, что мы увидим в полном коде. Таким образом, можно спокойно изучить каждый фрагмент. Результат выполнения данного фрагмента можно увидеть в следующей анимации:

Обратите внимание, что это довольно просто и понятно. В строке 21 мы объявили структуру. Следует помнить, что в рамках данной структуры у нас будет две переменные. Не следует рассматривать их таким образом. Не думайте, что это переменные, потому что это запутает вас и усложнит понимание других вещей в дальнейшем. Следует их рассматривать как поля или столбцы внутри ответа, возвращаемого SQL. Итак, у нас есть структура, которую следует рассматривать как строку ответа SQL. В этой строке результата ожидаются два поля.
Итак, в строке 28 мы пытаемся запросить данные из базы данных с помощью SQL. Обратите пристальное внимание на то, что находится в двойных кавычках ("), так как именно это должно быть командой, которую выполнит SQL. Вы должны написать именно то, что хотите передать на выполнение SQL. В этом простейшем примере нам нужны лишь все значения, присутствующие в таблице tb_Symbols. Хотя такой способ создания запросов не подходит для практического использования, для нашего примера он вполне годится, так как наша таблица очень мала и содержит мало записей.
Если наш запрос увенчается успехом, сначала мы выполним цикл for в строке 31. Теперь обратите внимание на то, что причиной завершения цикла станет именно отсутствие новых возвращаемых записей. И не забудьте заметить, что во время этих вызовов в строке 31 мы сообщаем функции GetRegisterOfRequest, что не хотим закрывать прочитанный блок. То есть, когда больше не останется доступных записей, мы не будем закрывать блок. Мы сохраним доступным полученный набор возвращенных записей.
Уже в строке 32 мы выведем в терминал возвращенный результат. В строке 33 мы указываем в терминале, что выполним повторное чтение данных, возвращенных SQL. Обратите внимание, что мы делаем практически то же самое, что и в строке 31. Однако на этот раз мы вообще ничего не будем сообщать во втором параметре вызова GetRegisterOfRequest. Таким образом, как только блок будет полностью прочитан, всё, что возвращается SQL-запросом, будет отброшено, и, следовательно, нам придется сделать новый запрос.
Следующий способ реализации показан в следующем фрагменте:
39. //+------------------------------------------------------------------+ 40. void SELECT_Type_02(void) 41. { 42. struct st 43. { 44. string of_day; 45. double price; 46. string symbol; 47. }stLocal; 48. 49. Print("Executing type 2 data request..."); 50. if ((*SQL).ExecRequestOfData("SELECT tq.of_day, tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 51. { 52. Print("Request: OK..."); 53. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 54. Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol); 55. } 56. Print("Finish type 2..."); 57. } 58. //+------------------------------------------------------------------+
Фрагмент основного кода
И результат можно увидеть в следующей анимации:

Обратите внимание, что здесь мы имеем немного другую картину. В первую очередь, в строке 42 мы имеем объявление структуры, которая будет иметь три поля. В предыдущем случае в каждой строке было по два поля. Однако прошу заметить, что, несмотря на увеличение количества полей, мы по-прежнему используем код, очень похожий на предыдущий. Однако на этот раз я хочу, чтобы вы внимательно посмотрели на команду SQL, используемую в строке 50. Там мы объединяем таблицы tb_Quotes и tb_Symbol, чтобы определенным образом представить то, что фактически существует в базе данных.
Теперь обратите внимание на порядок объявления полей в SQL-запросе. Это важно, потому что команда DatabaseReadBind будет соблюдать тот же порядок при чтении данных ответа SQL. Таким образом, если мы изменим порядок, либо в команде SQL, либо в объявлении полей внутри структуры в строке 42, мы можем увидеть совершенно неожиданные вещи в возвращаемых данных. Поэтому не упускайте такие детали.
Теперь перед нами случай, на первый взгляд, несколько более сложный. Но не поддавайтесь первому впечатлению: это так же просто, как и всё остальное. Мы лишь немного более подробно воспользуемся функциями, доступными в MQL5, цель которых — обеспечить возможность использования SQL внутри MQL5. Ниже можно ознакомиться с фрагментом.
58. //+------------------------------------------------------------------+ 59. void SELECT_Type_03(void) 60. { 61. struct st0 62. { 63. double price; 64. string symbol; 65. }stLocal; 66. struct st1 67. { 68. string tmp1, 69. tmp2, 70. date; 71. }std; 72. 73. Print("Executing type 3 data request..."); 74. if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol, tq.of_day FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 75. { 76. Print("Request: OK..."); 77. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++) 78. Print(" Resq ", c, " ==> { ", stLocal.price, " } ", stLocal.symbol); 79. Print("Reload..."); 80. for (int c = 0; (*SQL).GetRegisterOfRequest(std); c++) 81. Print(" Resq ", c, " ==> < ", std.date, " > "); 82. 83. } 84. Print("Finish type 3..."); 85. } 86. //+------------------------------------------------------------------+
Фрагмент основного кода
Результат выполнения данного фрагмента кода можно увидеть ниже.

До настоящего времени мы использовали структуру, соответствующую строке результата с двумя или тремя полями. Но, как мы уже объяснили в предыдущей теме, мы можем сделать гораздо больше, а именно заставить SQL возвращать больше полей, чем нам нужно. И, проанализировав должным образом поля, которые нам действительно были нужны, мы можем извлечь пользу из того, что SQL нам уже вернул. Таким образом, нам не придётся создавать новый запрос, чтобы найти нужные нам данные. Это связано с тем, что они уже пришли вместе с предыдущим запросом.
Вот важная деталь, которую необходимо понять. Суть в том, что каждая строка, возвращённая SQL будет соответствовать содержимому каждого конкретного поля. Но, как вы сможете убедиться в этом примере, мы выполним тот же самый запрос, что и в предыдущем фрагменте. Однако теперь SQL будет получать поля в другом порядке. Очень важно, чтобы вы обратили на это внимание. Причина заключается в том, что структура, объявленная в строке 61, содержит два поля. Структура, объявленная в строке 66, содержит три поля. SQL вернет три поля. Это происходит так, потому что команда запроса указывает SQL возвращать эти три поля.
Теперь обратите внимание, что структура st0 чередуется со структурой st1. Однако поле с именем date отсутствует в структуре st0 и поэтому игнорируется в тот момент, когда мы переходим к чтению данных, возвращенных SQL. Это происходит в строке 77, где мы создаём цикл для чтения и вывода данных, возвращаемых SQL, в терминал.
Однако в строке 80 мы делаем то же самое. Но на этот раз мы проигнорируем остальные данные, возвращенные SQL-запросом, и покажем только поле с названием date. Здесь нет каких-то больших сложностей. Всё дело в понимании того, что мы делаем и что нам сообщает SQL.
Вы, должно быть, уже думаете, что мы можем делать всё как угодно и что независимо от наших действий результат всё равно будет получен. Однако на самом деле всё не совсем так. Чтобы убедиться в этом, взгляните на следующий отрывок:
086. //+------------------------------------------------------------------+ 087. void SELECT_Type_04(void) 088. { 089. struct st 090. { 091. string of_day; 092. double price; 093. string symbol; 094. }stLocal; 095. 096. Print("Executing type 4 data request..."); 097. if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 098. { 099. Print("Request: OK..."); 100. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 101. Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol); 102. } 103. Print("Finish type 4..."); 104. } 105. //+------------------------------------------------------------------+
Фрагмент основного кода
Результат выполнения данного фрагмента показан ниже, и на первый взгляд он кажется несколько странным, поскольку мы получаем совершенно неожиданный результат.

Возможно, вам это покажется довольно странным. Но в этом нет ничего необычного: достаточно взглянуть на объявление в строке 89, где мы указываем, какие именно поля ожидаем получить в SQL-запросе. Теперь взгляните на строку 97 и на то, что мы запрашиваем у SQL. Обратите внимание, что информация не согласована между тем, что мы ожидаем получить в качестве результата, и тем, что мы запрашиваем у SQL.
Здесь возникает вопрос, из-за которого нам снова приходится обращаться к документации MQL5, чтобы понять, почему результат оказался именно таким, как показано на анимации. Итак, обратившись к документации основной функции процедуры GetRegisterOfRequest — а именно функции DatabaseReadBind, мы увидим следующий выделенный фрагмент:
Примечание
Количество полей в структуре struct_object не может превышать значение DatabaseColumnsCount(). Если количество полей в структуре struct_object меньше количества полей в записи, будет выполнено частичное чтение. Остальные данные можно получить явным образом, используя соответствующие функции DatabaseColumnText(), DatabaseColumnInteger() и так далее.
"Но подождите, мы же не используем DatabaseColumnsCount. Следовательно, результат не имеет смысла". Что ж, на самом деле в этом есть смысл, поскольку в SQL-запросе мы четко указываем, что хотим получить два поля. Обратите внимание, что в строке 97 мы запрашиваем поле price и поле symbol, каждое из которых извлекается из отдельной таблицы. Поскольку функция GetRegisterOfRequest не использует никаких методов для корректировки данных и заполнения двух из трех полей, указанных в структуре, функция DatabaseReadBind просто игнорирует любые попытки сопоставить данные с полями в структуре. Таким образом, результат в точности соответствует тому, что можно видеть в анимации.
Мы можем это улучшить, но пока останется как есть, поскольку нам нужно рассмотреть ещё один случай. Это можно увидеть в следующем фрагменте.
105. //+------------------------------------------------------------------+ 106. void SELECT_Type_05(void) 107. { 108. struct st 109. { 110. double price; 111. }stLocal; 112. 113. Print("Executing type 5 data request..."); 114. if ((*SQL).ExecRequestOfData("SELECT tq.price FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = (SELECT ts.id FROM tb_Symbols WHERE ts.symbol = 'PETR4') AND tq.of_day = '2023-07-11';")) 115. { 116. Print("Request: OK..."); 117. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 118. Print(" Resq ", c, " ==> { ", stLocal.price, " } "); 119. } 120. Print("Finish type 5..."); 121. } 122. //+------------------------------------------------------------------+
Фрагмент основного кода
В результате выполнения мы получаем именно это:

Это чуть более экстремальный случай использования SQL, когда мы заставляем его делать именно то, что вы, возможно, посчитали бы нужным сделать вне рамок SQL. Но здесь мы просто делаем очень специфический запрос, чтобы получить конкретную запись из базы данных. Теперь обратите внимание на строку 114. Она содержит команду, которую должен выполнить SQL. Прошу заметить, что мы запрашиваем котировки по инструменту PETR4, причем именно за 11.07.2023. Разумеется, дата соответствует формату, ожидаемому SQL. Но я думаю, что к этому моменту вы уже понимаете, как работает SQL.
Если посмотреть на базу данных, мы получим тот же результат, что и в терминале при выполнении запроса к SQL. В подобных ситуациях требуется, чтобы возвращалось только одно поле. Обратите внимание на то, как это объявляется в строке 108.
Но здесь может возникнуть вполне закономерный вопрос: нельзя ли просто использовать строку 110, без объявления структуры, показанной в строке 108? К сожалению, нет, уважаемый читатель. Не так, как была объявлена функция GetRegisterOfRequest. Дело в том, что если мы попытаемся использовать только строку 110, не создавая структуру, мы фактически не будем вызывать функцию чтения, выполненную с помощью функции DatabaseReadBind. Мы бы использовали что-то другое. Однако, будучи программистами, мы можем написать код, способный работать с подобными моментами, а также обрабатывать ситуацию, описанную в предыдущем фрагменте.
В любом случае, можно протестировать эти запросы — или, точнее говоря, SQL-команды, которые мы здесь рассмотрели — прямо в MetaEditor, чтобы ещё немного поэкспериментировать с созданием запросов, ориентированных на получение результатов, подобных тем, что были в этом последнем фрагменте. Но не забудьте протестировать то же самое в итоговом коде, который будет использоваться в качестве приложения MetaTrader 5. Полный код для тестирования, можно увидеть ниже:
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. #property description "Basic script for SQL database written in MQL5" 004. #property version "1.00" 005. #property script_show_inputs 006. //+------------------------------------------------------------------+ 007. #resource "\\Files\\Script 01.sql" as string SQL_Create 008. #resource "\\Files\\Script 02.sql" as string SQL_Insert 009. //+------------------------------------------------------------------+ 010. #include <Market Replay\SQL\C_DB_SQL.mqh> 011. //+------------------------------------------------------------------+ 012. enum eCall {eType_00, eType_01, eType_02, eType_03, eType_04, eType_05}; 013. //+------------------------------------------------------------------+ 014. input string user01 = "DataBase01"; //Database File Name 015. input ECall user02 = eType_01; //Search type 016. //+------------------------------------------------------------------+ 017. C_DB_SQL *SQL; 018. //+------------------------------------------------------------------+ 019. void SELECT_Type_01(void) 020. { 021. struct st 022. { 023. int id; 024. string symbol; 025. }stLocal; 026. 027. Print("Executing type 1 data request..."); 028. if ((*SQL).ExecRequestOfData("SELECT * FROM tb_Symbols")) 029. { 030. Print("Request: OK..."); 031. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++) 032. Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol); 033. Print("Reload..."); 034. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 035. Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol); 036. }; 037. Print("Finish type 1..."); 038. } 039. //+------------------------------------------------------------------+ 040. void SELECT_Type_02(void) 041. { 042. struct st 043. { 044. string of_day; 045. double price; 046. string symbol; 047. }stLocal; 048. 049. Print("Executing type 2 data request..."); 050. if ((*SQL).ExecRequestOfData("SELECT tq.of_day, tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 051. { 052. Print("Request: OK..."); 053. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 054. Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol); 055. } 056. Print("Finish type 2..."); 057. } 058. //+------------------------------------------------------------------+ 059. void SELECT_Type_03(void) 060. { 061. struct st0 062. { 063. double price; 064. string symbol; 065. }stLocal; 066. struct st1 067. { 068. string tmp1, 069. tmp2, 070. date; 071. }std; 072. 073. Print("Executing type 3 data request..."); 074. if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol, tq.of_day FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 075. { 076. Print("Request: OK..."); 077. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++) 078. Print(" Resq ", c, " ==> { ", stLocal.price, " } ", stLocal.symbol); 079. Print("Reload..."); 080. for (int c = 0; (*SQL).GetRegisterOfRequest(std); c++) 081. Print(" Resq ", c, " ==> < ", std.date, " > "); 082. 083. } 084. Print("Finish type 3..."); 085. } 086. //+------------------------------------------------------------------+ 087. void SELECT_Type_04(void) 088. { 089. struct st 090. { 091. string of_day; 092. double price; 093. string symbol; 094. }stLocal; 095. 096. Print("Executing type 4 data request..."); 097. if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 098. { 099. Print("Request: OK..."); 100. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 101. Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol); 102. } 103. Print("Finish type 4..."); 104. } 105. //+------------------------------------------------------------------+ 106. void SELECT_Type_05(void) 107. { 108. struct st 109. { 110. double price; 111. }stLocal; 112. 113. Print("Executing type 5 data request..."); 114. if ((*SQL).ExecRequestOfData("SELECT tq.price FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = (SELECT ts.id FROM tb_Symbols WHERE ts.symbol = 'PETR4') AND tq.of_day = '2023-07-11';")) 115. { 116. Print("Request: OK..."); 117. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 118. Print(" Resq ", c, " ==> { ", stLocal.price, " } "); 119. } 120. Print("Finish type 5..."); 121. } 122. //+------------------------------------------------------------------+ 123. const string ExecScripts(void) 124. { 125. string szMsg = (*SQL).ExecResourceSQL(SQL_Create); 126. if (szMsg != NULL) return szMsg; 127. return (*SQL).ExecResourceSQL(SQL_Insert); 128. }; 129. //+------------------------------------------------------------------+ 130. void OnStart() 131. { 132. string szMsg = NULL; 133. 134. SQL = new C_DB_SQL(user01); 135. 136. if (user02 == eType_00) szMsg = ExecScripts(); 137. Print(szMsg == NULL ? "Result of executing the SQL script: Success" : szMsg); 138. 139. if (szMsg == NULL) switch (user02) 140. { 141. case eType_01: SELECT_Type_01(); break; 142. case eType_02: SELECT_Type_02(); break; 143. case eType_03: SELECT_Type_03(); break; 144. case eType_04: SELECT_Type_04(); break; 145. case eType_05: SELECT_Type_05(); break; 146. } 147. 148. delete SQL; 149. } 150. //+------------------------------------------------------------------+
Основной код на MQL5
Обратите внимание, что все пояснения содержатся в коде, с той же нумерацией строк. Единственное небольшое отличие от кода, который мы видели в предыдущих статьях, — это строка 139, где, начиная с переменной, объявленной в строке 15, мы выбираем тип, или, точнее, какой фрагмент будет выполнен. Таким образом, можно видеть то же самое, что и в каждой из анимаций.
Заключительные идеи
В этой статье мы показали, как считывать данные, возвращаемые командой SQL при использовании MetaTrader 5 SQLite. Хотя, как вы могли заметить во время чтения статьи, мы всё ещё можем улучшить наш класс C_DB_SQL, по-прежнему ориентируясь на работу со встроенным SQLite в MetaTrader 5. Основные улучшения, которые мы можем внести, следующие:
- Позволить полям, определенным в структуре кода MQL5, заполняться даже в тех случаях, когда мы выполняем запрос с меньшим количеством столбцов. Это позволило бы избежать неприятного результата, когда вызов GetRegisterOfRequest не возвращает никакой информации.
- Другим улучшением стало бы то, чтобы в случае запроса, результатом которого является одна-единственная интересующая нас запись (как это произошло во фрагменте SELECT_Type_05), нам не приходилось объявлять возвращаемую переменную внутри структуры, а можно было использовать её напрямую как стандартную и независимую переменную.
Хотя внедрение этих улучшений представляет интерес, я пока не уверен, буду ли показывать, как это сделать. Потому что при программировании приложения на языке MQL5 мы можем действовать несколько более осмотрительно. В любом случае, если будет решено показать, как внести эти изменения для лучшей поддержки случаев, указанных во фрагментах, это будет сделано в следующей статье. Но независимо от этого, обязательно следите за выходом следующей статьи из этой серии, ведь тема становится всё более интересной с каждым днём.
| Файл | Описание |
|---|---|
| Experts\Expert Advisor.mq5 | Демонстрирует взаимодействие между Chart Trade и советником (для взаимодействия требуется Mouse Study). |
| Indicators\Chart Trade.mq5 | Создает окно для настройки отправляемого ордера (для взаимодействия требуется Mouse Study). |
| Indicators\Market Replay.mq5 | Создает элементы управления для взаимодействия с сервисом репликации/моделирования (для взаимодействия требуется Mouse Study) |
| Indicators\Mouse Study.mq5 | Обеспечивает взаимодействие между графическими элементами управления и пользователем (необходимо как для работы системы репликации/моделирования, так и на реальном рынке). |
| Services\Market Replay.mq5 | Создает и поддерживает сервис репликации/моделирования рынка (главный файл всей системы) |
| Code VS C++\Servidor.cpp | Создает и поддерживает сокет-сервер, разработанный на C++ (версия мини-чата). |
| Code in Python\Server.py | Создает и поддерживает сокет в Python для связи между MetaTrader 5 и Excel |
| Indicators\Mini Chat.mq5 | Позволяет реализовать мини-чат через индикатор (для работы требуется использование сервера) |
| Experts\Mini Chat.mq5 | Позволяет реализовать мини-чат с помощью советника (для работы требуется сервер). |
| Scripts\SQLite.mq5 | Демонстрирует использование скрипта SQL с помощью MQL5 |
| Files\Script 01.sql | Демонстрирует создание простой таблицы с внешним ключом. |
| Files\Script 02.sql | Показывает добавление значений в таблицу |
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/13117
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Торговые инструменты MQL5 (Часть 23): Трёхмерные графики с управляемой камерой и поддержкой DirectX для анализа распределений
Нативная реализация RSA-шифрования на MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования