Интеграция эксперта на MQL и базы данных (SQL Server, .NET и C#)

11 июля 2018, 12:43
Сергей Ткаченко
6
2 304

Введение. Эксперты на MQL и базы данных

На форумах периодически встречаются вопросы о том, как интегрировать в экспертов, написанных на MQL 5, работу с базами данных. Интерес к этой теме неудивителен. БД очень хороши как средства сохранения данных. В отличие от логов терминала, данные из баз никуда не исчезают. Их легко сортировать и фильтровать, выбирая только нужные. Через БД эксперту можно передавать нужную информацию — например, те или иные команды. И главное — полученные данные можно анализировать с разных сторон и статистически обрабатывать. Например, чтобы узнать среднюю и суммарную прибыль за указанное время по каждой валютной паре, достаточно написать запрос в одну строку, что займёт буквально минуту. А теперь представьте, сколько потребуется времени, чтобы подсчитать это вручную по истории счёта в торговом терминале.

К сожалению, штатных средств для взаимодействия с серверами БД в терминалах MetaTrader нет. Проблема решается только через импорт функций из DLL-файлов. Задача непростая, но выполнимая.

Мне не один раз приходилось это делать, и в этой статье я решил поделиться опытом. В качестве примера опишу организацию взаимодействия экспертов на MQL5 с сервером баз данных Microsoft SQL Server. Для создания DLL-файла, из которого эксперты будут импортировать функции для работы с БД, использовалась платформа Microsoft.NET и язык программирования C#. В статье я подробно описал создание проекта и подготовку DLL-файла, а затем — импорт функций из него в эксперт, написанный на языке MQL5. Приводимый для примера код эксперта очень прост. Чтобы скомпилировать его в MQL4, нужно внести в него минимальные изменения.

Подготовка к работе

Для работы нам потребуется следующее.

  1. Установленный MetaTrader 5 и активный торговый счёт. Можно использовать не только демо-счёт, но и реальный — используя тестовый эксперт, вы не рискуете своим депозитом.
  2. Установленный экземпляр сервера БД Microsoft SQL Server. Можно использовать сервер БД на другом компьютере, подключаясь к нему по сети. Можно скачать с сайта Microsoft и установить бесплатную версию Express Edition — для большинства пользователей её ограничения несущественны. Скачать её можно, например, вот тут: https://www.microsoft.com/ru-ru/sql-server/sql-server-editions-express.  Компания Microsoft иногда меняет адреса ссылок на своём сайте, поэтому если прямая ссылка не будет работать — просто наберите в любом поисковике фразу наподобие "SQL Server Express скачать". Если вы устанавливаете SQL Server в первый раз, то могут возникнуть некоторые сложности с установкой. В частности, на старых версиях ОС он может попросить установить дополнительные компоненты (в частности, PowerShell и .NET 4.5). Также иногда возникает конфликт SQL Server и VS C++ 2017, при этом установщик просит восстановить С++. Это можно сделать через "Панель управления", "Программы", "Программы и компоненты", "VS C++ 2017", "Изменить", "Восстановить". Проблемы бывают не всегда и обычно легко решаются.
  3. Среда разработки, использующая .NET и C#. Я использую Microsoft Visual Studio (для которой есть и бесплатная версия), поэтому примеры буду приводить именно для нее. Можно использовать другую среду разработки и даже другой язык программирования. Но тогда вам придётся самим думать, как реализовать приводимые примеры в вашей среде и на выбранном вами языке.
  4. Средство экспорта функций из DLL-файлов .NET в неуправляемый код. Эксперты на MQL не умеют работать с управляемым кодом .NET. Поэтому полученный DLL-файл надо будет специально подготовить, обеспечив возможность экспорта функций. В Сети описаны разные способы это сделать. Я применял пакет "UnmanagedExports", который создал программист Robert Giesecke. Если вы используете Microsoft Visual Studio версии 2012 и выше, то можно добавить его в проект прямо из меню среды разработки. Как это сделать — я расскажу далее.

Кроме установки необходимых программ, нужно выполнить ещё одну подготовительную операцию. По ряду причин пакет "UnmanagedExports" не может работать, если в языковых настройках вашего компьютера в качестве языка для программ, не поддерживающих Юникод, выбран "Русский (Россия)". Могут возникнуть проблемы и с другими языками, если это не "Английский (США)". Чтобы его установить, откройте панель управления. Там найдите вкладку "Язык и региональные стандарты", оттуда перейдите на вкладку "Дополнительно". На вкладке "Язык программ, не поддерживающих Юникод" жмём "Изменить язык системы...". Если там установлено "Английский (США)" — всё хорошо. Если что-то другое — поменяйте на "Английский (США)" и перезагрузите компьютер.

Если этого не сделать, то при компиляции проекта на этапе выполнения скриптов "UnmanagedExports" будут выдаваться синтаксические ошибки в ".il"-файлах. Исправить их нельзя. Даже если ваш проект будет совсем простым и ошибок в коде на C# точно не будет — в ".il"-файлах ошибки всё равно будут появляться, и вы не сможете экспортировать функции из проекта в неуправляемый код.

Это касается только 64-битных приложений. 32-битные можно обрабатывать другими средствами, для которых язык системы менять не нужно. Например, подойдет программа DllExporter.exe, которую можно скачать по ссылке: https://www.codeproject.com/Articles/37675/Simple-Method-of-DLL-Export-without-C-CLI.

Изменение языка системы сделает неработоспособными некоторые приложения. К сожалению, с этими неудобствами придётся смириться, но это ненадолго. Изменение языка нужно только при компиляции проекта. После успешной компиляции язык можно будет поменять обратно. 

Создание DLL-файла

Открываем Visual Studio и создаём новый проект Visual C#, выбрав в качестве его типа "Class Library". Назовём его MqlSqlDemo. В свойствах проекта, в разделе "Build", надо обязательно настроить целевую платформу ("Platform target"). Там надо заменить "Any CPU" на "x64" (и в конфигурации Debug, и в конфигурации Release). Это связано с особенностями экспорта функций в неуправляемый код — им обязательно надо явно указать тип процессора.

Версию фреймворка .NET ставим 4.5. Обычно она уже выбрана  по умолчанию.

При создании проекта в него сразу же, автоматически добавляется файл "Class1.cs", содержащий класс "Class1". Переименуем и файл, и класс в "MqlSqlDemo.cs" и "MqlSqlDemo". Функции, которые будут экспортироваться из DLL-файла, могут быть только статическими — это опять же требуется для экспорта в неуправляемый код.

Строго говоря, можно экспортировать и нестатические функции. Но для этого нужно обратиться к средствам C++/CLI, которые в этой статье мы не рассматриваем.

Поскольку все функции в нашем классе обязательно должны быть статическими, то сам класс тоже имеет смысл сделать статическим. В этом случае, если для какой-то функции будет забыт модификатор "static" — это сразу же выяснится при компиляции проекта. Получим следующее описание класса:

public static class MqlSqlDemo
{
    // ...
}

Теперь надо настроить зависимости проекта (раздел "References" в "Solution Explorer"). Убираем оттуда всё лишнее, оставив только "System" и "System.Data".

Теперь добавим пакет "UnmanagedExports".

Описание пакета можно посмотреть на сайте его автора: https://sites.google.com/site/robertgiesecke/Home/uploads/unmanagedexports.

Добавлять его удобнее всего через пакетный менеджер NuGet. Инструкции по добавлению можно найти на сайте NuGet: https://www.nuget.org/packages/UnmanagedExports

Нам из этих инструкций нужна только одна:

Install-Package UnmanagedExports -Version 1.2.7

В меню Visual Studio выбираем раздел "Tools", в нём — пункт "NuGet Package Manager", а далее — "Package Manager Console". Внизу откроется командная строка. В неё нужно вставить скопированную инструкцию "Install-Package UnmanagedExports -Version 1.2.7" и нажать клавишу "Enter". Пакетный менеджер некоторое время будет подключаться к интернету и скачивать пакет, а потом добавит его к проекту и выдаст следующее:

PM> Install-Package UnmanagedExports -Version 1.2.7
Installing 'UnmanagedExports 1.2.7'.
Successfully installed 'UnmanagedExports 1.2.7'.
Adding 'UnmanagedExports 1.2.7' to MqlSqlDemo.
Successfully added 'UnmanagedExports 1.2.7' to MqlSqlDemo.

PM> 

Это значит, что пакет был успешно добавлен.

После этого можно переходить непосредственно к написанию кода в файле описания класса MqlSqlDemo.cs. 

Настроим используемые пространства имён.

  • Visual Studio добавляет много лишнего. Убираем из раздела "using" всё, кроме "using System;".
  • Теперь добавляем "using System.Data;" — отсюда будут браться классы для работы с базами данных.
  • Добавляем "using System.Data.SqlClient;": здесь содержатся классы для работы конкретно с БД SQL Server.
  • Добавляем "using System.Runtime.InteropServices;" — здесь содержатся атрибуты для взаимодействия с неуправляемым кодом.
  •  Добавляем "using RGiesecke.DllExport;" — отсюда будем брать атрибут, которым нужно помечать экспортируемые функции.
Получается такой набор:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Runtime.InteropServices;
using RGiesecke.DllExport;

Добавим нужные переменные. Переменные в статическом классе тоже могут быть только статическими. Нам понадобятся объекты для работы с БД — объект соединения и объект команды: 

private static SqlConnection conn = null;
private static SqlCommand com = null;

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

private static string sMessage = string.Empty;

Объявим две константы со значениями 0 и 1 — они будут служить возвращаемыми значениями для большинства функций. При успешном выполнении функции будут возвращать 0, при ошибке — 1. Это сделает код более понятным.

public const int iResSuccess = 0;
public const int iResError = 1;

Теперь перейдём к функциям.

Для функций, которые будут экспортироваться для использования в MQL5, есть ограничения.

  1. Функции, как уже говорилось, должны быть статическими.
  2. Нельзя использовать шаблонные классы коллекций (пространство имён System.Collections.Generic). Компилироваться с ними всё будет хорошо, но на этапе исполнения могут возникать необъяснимые ошибки. Использование этих классов возможно в других функциях, которые не будут экспортироваться, но лучше вообще обойтись без них. Можно использовать обычные массивы. Наш проект написан только для ознакомительных целей, поэтому в нём таких классов (как, впрочем, и массивов) не будет.

В нашем демо-проекте передавать будем только простые данные — числа или строки. Также теоретически можно было бы передавать значения типа boolean, которые по своему внутреннему представлению тоже являются целыми числами. Но значения этих чисел могут по-разному интерпретироваться разными системами (MQL и .NET). Это приводит к ошибкам. Поэтому ограничимся тремя типами данных — int, string и double. Значения boolean, если возникнет такая необходимость, надо передавать как int.

В реальных проектах можно передавать сложные структуры данных, но для организации работы с SQL Server вполне можно обойтись без этого.

Для работы с БД нам прежде всего потребуется установить соединение. Для этого служит функция CreateConnection. Функция будет принимать один параметр — строку с параметрами подключения к БД SQL Server. Возвращать она будет целое число, показывающее, удалось ли установить соединение. При успешном подключении будем возвращать iResSuccess, т.е. 0. При неудаче — iResError, т.е. 1. Более подробную информацию об ошибке будем заносить в строку сообщения — sMessage.

Вот что получается:

[DllExport("CreateConnection", CallingConvention = CallingConvention.StdCall)]
public static int CreateConnection(
        [MarshalAs(UnmanagedType.LPWStr)] string sConnStr)
{
        // Очищаем строку сообщения:
        sMessage = string.Empty;
        // Если соединение уже есть - закрываем его и меняем
        // строку подключения на новую, если нет -
        // создаём заново объекты подключения и команды:
        if (conn != null)
        {
                conn.Close();
                conn.ConnectionString = sConnStr;
        }
        else
        {
                conn = new SqlConnection(sConnStr);
                com = new SqlCommand();
                com.Connection = conn;
        }
        // Пробуем открыть подключение:
        try
        {
                conn.Open();
        }
        catch (Exception ex)
        {
                // Почему-то подключение не открылось.
                // Заносим информацию об ошибке в строку сообщения:
                sMessage = ex.Message;
                // Освобождаем ресурсы и обнуляем объекты:
                com.Dispose();
                conn.Dispose();
                conn = null;
                com = null;
                // Ошибка:
                return iResError;
        }
        // Всё прошло хорошо, подключение открыто:
        return iResSuccess;
}

Атрибутом DllExport перед описанием функции помечается каждая экспортируемая функция. Он находится в пространстве имён RGiesecke.DllExport, импортируемом из сборки RGiesecke.DllExport.Metadata. Сборка добавляется в зависимости проекта автоматически, когда пакетный менеджер NuGet устанавливает пакет UnmanagedExports. Этому атрибуту нужно передать два параметра: 

  • название функции, под которым она будет экспортироваться. По этому названию её будут вызывать из DLL внешние программы, в том числе MetaTrader 5. Можно сделать название экспортируемой функции таким же, как и название функции в коде — CreateConnection;
  • второй параметр показывает, какой механизм вызова функций будет использоваться. Для всех наших функций подойдёт CallingConvention.StdCall.

Обратим внимание и на атрибут [MarshalAs(UnmanagedType.LPWStr)]. Он стоит перед параметром строки подключения ConnStringIn, который принимает функция. Этот атрибут показывает, в каком виде должна передаваться строка. На момент написания этой статьи MetaTrader 5 и MetaTrader 4 работают со строками в юникоде — UnmanagedType.LPWStr.

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

Используемый для подключения метод Open не возвращает никакого результата. Поэтому узнать, было ли подключение успешным, можно только путём перехвата исключений. В случае ошибки освобождаем ресурсы, обнуляем объекты, заносим информацию в строку сообщения и возвращаем iResError. Если всё идёт хорошо — возвращаем iResSuccess.

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

[DllExport("GetLastMessage", CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.LPWStr)]
public static string GetLastMessage()
{
        return sMessage;
}

Как и функция установки соединения, эта функция тоже помечена атрибутом экспорта DllExport. Атрибут [return: MarshalAs(UnmanagedType.LPWStr)] показывает, каким образом должен передаваться возвращаемый результат. Так как результат — строка, то передавать её в MetaTrader 5 нужно тоже в Юникоде. Поэтому здесь тоже используем UnmanagedType.LPWStr.

После открытия подключения можно начинать работу с БД. Добавим возможность выполнять запросы к БД. Этим будет заниматься функция ExecuteSql:

[DllExport("ExecuteSql", CallingConvention = CallingConvention.StdCall)]
public static int ExecuteSql(
        [MarshalAs(UnmanagedType.LPWStr)] string sSql)
{
        // Очищаем строку сообщения:
        sMessage = string.Empty;
        // Сначала нужно проверить, установлено ли подключение.
        if (conn == null)
        {
                // Соединение ещё не открыто.
                // Сообщаем об ошибке и возвращаем флаг ошибки:
                sMessage = "Connection is null, call CreateConnection first.";
                return iResError;
        }
        // Соединение готово, пробуем выполнить команду.
        try
        {
                com.CommandText = sSql;
                com.ExecuteNonQuery();
        }
        catch (Exception ex)
        {
                // Ошибка при выполнении команды.
                // Заносим информацию об ошибке в строку сообщения:
                sMessage = ex.Message;
                // Возвращаем флаг ошибки:
                return iResError;
        }
        // Всё прошло хорошо - возвращаем флаг успешного выполнения:
        return iResSuccess;
}

Текст запроса передаётся в функцию параметром. Перед выполнением запроса проверяем, открыто ли у нас подключение. Как и в функции открытия соединения, в случае успешного выполнения функция возвращает iResSuccess, в случае ошибки — iResError. Чтобы получить более подробную информацию о причинах ошибки, нужно использовать функцию GetLastMessage. С использованием функции ExecuteSql можно выполнять любые запросы — записывать данные, удалять, изменять их. Можно даже работать со структурой БД. Но читать данные, к сожалению, она не позволяет — функция не возвращает результат и никуда не сохраняет прочитанные данные. Запрос будет выполнен, но увидеть то, что было прочитано, не удастся. Поэтому добавим ещё две функции для чтения данных.

Первая функция предназначена для чтения из таблицы БД одного целого числа.

[DllExport("ReadInt", CallingConvention = CallingConvention.StdCall)]
public static int ReadInt(
        [MarshalAs(UnmanagedType.LPWStr)] string sSql)
{
        // Очищаем строку сообщения:
        sMessage = string.Empty;
        // Сначала нужно проверить, установлено ли подключение.
        if (conn == null)
        {
                // Соединение ещё не открыто.
                // Сообщаем об ошибке и возвращаем флаг ошибки:
                sMessage = "Connection is null, call CreateConnection first.";
                return iResError;
        }
        // Переменная для получения возвращаемого результата:
        int iResult = 0;
        // Соединение готово, пробуем выполнить команду.
        try
        {
                com.CommandText = sSql;
                iResult = (int)com.ExecuteScalar();
        }
        catch (Exception ex)
        {
                // Ошибка при выполнении команды.
                // Заносим информацию об ошибке в строку сообщения:
                sMessage = ex.Message;
        }
        // Возвращаем полученный результат:
        return iResult;
}

Реализовать чтение данных значительно сложнее, чем просто выполнение команд. Эта функция сильно упрощена и использует функцию ExecuteScalar класса SqlCommand. Она возвращает значение первого столбца первой строки, возвращаемой запросом. Поэтому передаваемый параметром SQL-запрос должен быть сформирован таким образом, чтобы в возвращаемом им наборе данных были строки, а в первом столбце было целое число. Кроме того, функция должна как-то возвращать прочитанное число. Поэтому ее результатом уже не будет сообщение об успешности выполнения. Чтобы понять, удалось ли выполнить запрос и прочитать данные, нужно будет в любом случае анализировать последнее сообщение, вызывая GetLastMessage. Если последнее сообщение пустое — значит, ошибки не было и данные были прочитаны. Если там что-то записано — это означает, что произошла ошибка и данные прочитать не удалось.

Вторая функция тоже читает из БД одно значение, но другого типа — не целое число, а строку. Строки можно читать так же, как и числа, разница — только в типе возвращаемого результата. Поскольку функция возвращает строку, надо пометить ее атрибутом [return: MarshalAs(UnmanagedType.LPWStr)]. Вот код этой функции:

[DllExport("ReadString", CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.LPWStr)]
public static string ReadString(
        [MarshalAs(UnmanagedType.LPWStr)] string sSql)
{
        // Очищаем строку сообщения:
        sMessage = string.Empty;
        // Сначала нужно проверить, установлено ли подключение.
        if (conn == null)
        {
                // Соединение ещё не открыто.
                // Сообщаем об ошибке и возвращаем флаг ошибки:
                sMessage = "Connection is null, call CreateConnection first.";
                return string.Empty;
        }
        // Переменная для получения возвращаемого результата:
        string sResult = string.Empty;
        // Соединение готово, пробуем выполнить команду.
        try
        {
                com.CommandText = sSql;
                sResult = com.ExecuteScalar().ToString();
        }
        catch (Exception ex)
        {
                // Ошибка при выполнении команды.
                // Заносим информацию об ошибке в строку сообщения:
                sMessage = ex.Message;
        }
        // Возвращаем полученный результат:
        return sResult;
}

Для демо-проекта такого чтения данных будет достаточно. Для реального эксперта оно вообще может не понадобиться — экспертам более важно записывать данные в БД для последующего анализа. Если читать данные всё-таки нужно, то эти функции вполне можно использовать — они вполне работоспособны. Однако порой нужно бывает прочитать много строк из таблицы, содержащей несколько столбцов.

Для этого можно пойти двумя путями. Можно возвращать из функции сложные структуры данных (этот путь не подойдёт для MQL4). Также можно в нашем классе объявить статическую переменную класса DataSet. При чтении надо будет загружать данные из БД в этот DataSet, а потом уже другими функциями читать данные оттуда по одной ячейке за один вызов функции. Такой подход реализован в упоминаемом ниже проекте HerdOfRobots. Его можно подробно изучить в коде проекта, а мы, чтобы не раздувать объем статьи, не будем обсуждать многострочное чтение данных.

После завершения работы с БД соединение нужно будет закрыть, освободив используемые ресурсы. Для этого предназначена функция CloseConnection:

[DllExport("CloseConnection", CallingConvention = CallingConvention.StdCall)]
public static void CloseConnection()
{
        // Сначала нужно проверить, установлено ли подключение.
        if (conn == null)
                // Соединение ещё не открыто - значит и закрывать его тоже не нужно:
                return;
        // Соединение открыто - его надо закрыть:
        com.Dispose();
        com = null;
        conn.Close();
        conn.Dispose();
        conn = null;
}

Эта простая функция не принимает никаких параметров и не возвращает результат.

Все нужные функции готовы. Компилируем проект.

Поскольку функции нужно будет использовать не из других приложений .NET, а из MetaTrader (который .NET не использует), то компиляция будет проходить в два этапа.  На первом этапе всё делается так же, как и для всех .NET-проектов. Создается обычная сборка, потом она обрабатывается пакетом UnmanagedExports. Работа пакета начинается уже после компиляции сборки. Сначала запускается IL-декомпилер, который разбирает полученную сборку на IL-код. Затем IL-код изменяется — оттуда удаляются ссылки на атрибуты DllExport и добавляются инструкции по экспорту помеченных этим атрибутом функций. После этого файл с IL-кодом повторно компилируется и записывается вместо исходной DLL.

Все эти действия выполняются автоматически. Но, как уже говорилось выше, если в настройках операционной системы выбран русский язык для программ, не поддерживающих Юникод, то при попытке скомпилировать файл с модифицированным IL-кодом пакет UnmanagedExports, скорее всего, выдаст ошибку и сделать ничего не сможет.

Если при компиляции не появилось сообщений об ошибках — значит, всё прошло хорошо и полученную DLL можно использовать в экспертах. Кроме того, UnmanagedExports при успешной обработке DLL добавляет ещё два файла с расширениями ".exp" и ".lib" (в нашем случае это будут "MqlSqlDemo.exp" и "MqlSqlDemo.lib"). Нам они не понадобятся, но по их наличию можно судить о том, что работа UnmanagedExports  завершилась успешно.

Надо отметить, что демо-проект имеет очень существенное ограничение: позволяет запускать только одного эксперта, работающего с БД, в одном терминале MetaTrader. Дело в том, что все эксперты используют один экземпляр загруженной DLL. Поскольку наш класс сделан статическим, то он будет один для всех запущенных экспертов. Переменные тоже будут общими. Если запустить нескольких экспертов, то все они будут использовать одно и то же подключение и один объект команды на всех.  Если к этим объектам попытаются одновременно обращаться сразу несколько экспертов — могут возникнуть проблемы.

Но для объяснения принципов работы и для тестирования подключения к БД такого проекта будет вполне достаточно. Теперь у нас есть DLL-файл с функциями. Можно приступать к написанию эксперта на MQL5. 

Создание эксперта на MQL5

Сделаем простого эксперта на MQL5. Его код также можно скомпилировать в редакторе MQL4, если поменять его расширение с "mq5" на "mq4".  Эксперт нужен только для демонстрации успешной работы с БД, поэтому никаких торговых операций выполнять не будет.

Запускаем MetaEditor, нажимаем кнопку "Создать". Оставляем пункт "Советник (Шаблон)" и нажимаем "Далее". Указываем название "MqlSqlDemo". Также добавляем один параметр — "ConnectionString" типа "string". Это будет строка подключения, указывающая, как подключиться к вашему серверу БД. Начальное значение для неё можно указать, например, такое:

Server=localhost;Database=master;Integrated Security=True

Эта строка подключения позволяет подключиться к неименованному ("Default Instance") серверу БД, установленному на том же компьютере, на котором работает MetaTrader. Логин и пароль при этом указывать не нужно — используется авторизация по учётной записи ОС Windows.

Если вы скачали SQL Server Express и установили его на свой компьютер, при установке не меняя параметров, то ваш SQL Server будет "именованным экземпляром". Он получит имя "SQLEXPRESS". Строка подключения у него будет другая:

Server=localhost\\SQLEXPRESS;Database=master;Integrated Security=True

При добавлении строкового параметра в шаблон советника есть ограничение по размеру строки. Более длинная строка подключения (например, к именованному серверу "SQLEXPRESS") может не поместиться. Но проблемы с этим нет — значение параметра на этом этапе можно вообще оставить пустым. Потом, уже при редактировании кода эксперта, можно сделать её какой угодно. Также можно задавать нужную строку подключения при запуске эксперта.

Жмём "Далее". Никаких функций добавлять не нужно, поэтому на следующем экране оставляем все галочки неотмеченными. Опять жмём "Далее" и получаем сгенерированный начальный код эксперта.

Эксперт нужен только для демонстрации подключения к БД и работы с ней. Для этого достаточно только функции инициализации — OnInit. Заготовки для остальных функций — OnDeinit и OnTick — можно сразу же убрать.

В результате у меня получилось следующее:

//+------------------------------------------------------------------+
//|                                                   MqlSqlDemo.mq5 |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+

#property copyright "Copyright 2018, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict
//--- input parameters
input string   ConnectionString = "Server=localhost\\SQLEXPRESS;Database=master;Integrated Security=True";
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
  
//---
   return(INIT_SUCCEEDED);
  }

Обратите внимание: при подключении к именованному экземпляру (в нашем случае "SQLEXPRESS") надо повторять символ "\" дважды: "localhost\\SQLEXPRESS". Это требуется и при добавлении параметра в шаблоне советника, и в коде. Если указать символ только один раз, то компилятор решит, что в строке указана Escape-последовательность (специальный символ) "\S", и при компиляции сообщит, что она не распознана.

Но если перенести на график уже скомпилированного робота, то в его параметрах будет только один символ "\", несмотря на то, что в коде их указано два. Дело в том, что при компиляции все Escape-последовательности в строках преобразуются в соответствующие символы. Последовательность "\\" преобразуется в один символ "\", и пользователи (которым работать с кодом не нужно) видят уже обычную строку. Поэтому, если вы задаёте строку подключения не в коде, а при запуске советника — тогда в строке подключения указывается только один символ "\":

Server=localhost\SQLEXPRESS;Database=master;Integrated Security=True

Теперь добавим функциональность к заготовке эксперта. Сначала нужно импортировать из созданной DLL функции для работы с БД. Добавляем перед функцией OnInit раздел импорта. Импортируемые функции описываются почти так же, как они объявлены в коде на C#, надо только убрать все модификаторы и атрибуты:

// Описание импортируемых функций.
#import "MqlSqlDemo.dll"

// Функция открытия соединения:
int CreateConnection(string sConnStr);
// Функция чтения последнего сообщения:
string GetLastMessage();
// Функция выполнения SQL-команды:
int ExecuteSql(string sSql);
// Функция чтения целого числа:
int ReadInt(string sSql);
// Функция чтения строки:
string ReadString(string sSql);
// Функция закрытия соединения:
void CloseConnection();

// Завершение импорта:
#import

Для большей понятности кода объявим константы результатов выполнения функций. Как и в DLL, это будут 0 при успешном выполнении и 1 при ошибке:

// Успешное выполнение функции:
#define iResSuccess  0
// Ошибка при выполнении функции:
#define iResError 1

Теперь можно добавлять в функцию инициализации OnInit вызовы функций, которые будут работать с БД. Вот как она будет выглядеть:

int OnInit()
  {
   // Пробуем открыть соединение:
   if (CreateConnection(ConnectionString) != iResSuccess)
   {
      // Подключение установить не удалось.
      // Пишем сообщение и завершаем работу:
      Print("Ошибка при открытии соединениия. ", GetLastMessage());
      return(INIT_FAILED);
   }
   Print("Соединение с БД установлено.");
   // Подключение успешно установлено.
   // Пробуем выполнить запросы.
   // Создадим таблицу и запишем в неё данные:
   if (ExecuteSql(
      "create table DemoTest(DemoInt int, DemoString nvarchar(10));")
      == iResSuccess)
      Print("Создана таблица в БД.");
   else
      Print("Не получилось создать таблицу. ", GetLastMessage());
   if (ExecuteSql(
      "insert into DemoTest(DemoInt, DemoString) values(1, N'Test');")
      == iResSuccess)
      Print("Данные записаны в таблицу.");
   else
      Print("Не получилось записать данные в таблицу. ", GetLastMessage());
   // Переходим к чтению данных. Прочитаем целое число из БД:
   int iTestInt = ReadInt("select top 1 DemoInt from DemoTest;");
   string sMessage = GetLastMessage();
   if (StringLen(sMessage) == 0)
      Print("Число прочитано из БД: ", iTestInt);
   else // Не получилось прочитать число.
      Print("Не получилось прочитать число из БД. ", GetLastMessage());
   // Теперь прочитаем строку:
   string sTestString = ReadString("select top 1 DemoString from DemoTest;");
   sMessage = GetLastMessage();
   if (StringLen(sMessage) == 0)
      Print("Строка прочитана из БД: ", sTestString);
   else // Не получилось прочитать строку.
      Print("Не получилось прочитать строку из БД. ", GetLastMessage());
   // Таблица больше не нужна - её можно удалить.
   if (ExecuteSql("drop table DemoTest;") != iResSuccess)
      Print("Не получилось удалить таблицу. ", GetLastMessage());
   // Завершили работу - закрываем подключение:
   CloseConnection();
   // Завершаем инициализацию:
   return(INIT_SUCCEEDED);
  }

Компилируем эксперта. Всё, тестовый эксперт готов. Можно его запускать. Перед запуском необходимо добавить DLL в папку библиотек используемого вами профиля MetaTrader. Запускаем MetaTrader, в меню "Файл" выбираем "Открыть каталог данных". Открываем папку "MQL5" (в случае MetaTrader 4 это будет папка "MQL4"), в ней — папку "Libraries". Кладём туда файл нашей DLL — MqlSqlDemo.dll. Эксперт к этому времени должен уже быть скомпилирован и доступен для использования. Конечно же, запуск экспертов и импорт функций из DLL должны быть разрешены в настройках MetaTrader 5 — иначе запуск эксперта сразу же завершится ошибкой.

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

2018.07.10 20:36:21.428    MqlSqlDemo (EURUSD,H1)    Соединение с БД установлено.
2018.07.10 20:36:22.187    MqlSqlDemo (EURUSD,H1)    Создана таблица в БД.
2018.07.10 20:36:22.427    MqlSqlDemo (EURUSD,H1)    Данные записаны в таблицу.
2018.07.10 20:36:22.569    MqlSqlDemo (EURUSD,H1)    Число прочитано из БД: 1
2018.07.10 20:36:22.586    MqlSqlDemo (EURUSD,H1)    Строка прочитана из БД: Test

 Подключение к БД, выполнение SQL-команд, запись и чтение данных — всё успешно выполняется.

Заключение

Полное решение для Visual Studio — архив, содержащий все необходимые файлы, прилагается к статье под названием "MqlSqlDemo.zip". Пакет "UnmanagedExports" уже установлен. Тестовый эксперт MqlSqlDemo.mq5 и его вариант для MQL4 тоже находятся во вложенной папке "MQL".

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

Созданные в рамках этой статьи файл DLL и эксперт предназначены только для обучения и ознакомительных целей. Использовать DLL в реальных проектах, конечно же, можно. Но, скорее всего, довольно быстро он перестанет вас устраивать из-за существенных ограничений. Если вы хотите добавить возможность работы с БД к своим экспертам — скорее всего, вам потребуется больше возможностей. Тогда надо будет дописывать код самостоятельно, руководствуясь статьёй как примером. Если у вас возникнут трудности — пишите о них в комментариях к статье, на мой Skype cansee378 или через контактную страницу моего сайта: http://life-warrior.org/contact.

Если же у вас нет времени или желания разбираться с кодом на C# — можно скачать уже готовый проект. Бесплатная программа с открытым кодом HerdOfRobots реализована на тех же принципах, которые описаны в статье. В комплект установки вместе с программой входят готовые файлы с импортируемыми функциями для MetaTrader 5 и MetaTrader 4. Эти библиотеки обладают значительно более широкими возможностями. Например, они позволяют запускать до 63 экспертов в одном терминале, подключаемых к разным БД, построчно читать данные из таблиц, записывать в БД значения даты/времени.

Сама программа HerdOfRobots предоставляет удобные возможности по контролю экспертов, подключающихся к БД, и анализу записываемых ими данных. В комплекте прилагается справка, подробно описывающая все аспекты работы. Архив с установочным файлом программы — SetupHerdOfRobots.zip — я тоже прилагаю к статье. Если вы хотите посмотреть код программы и используемого для подключения к БД проекта MqlToSql64 (MqlToSql для MT4), чтобы затем использовать упомянутые продвинутые возможности в своих проектах — код можно скачать в открытых репозиториях:

https://bitbucket.org/CanSeeThePain/herdofrobots

https://bitbucket.org/CanSeeThePain/mqltosql64

https://bitbucket.org/CanSeeThePain/mqltosql

Прикрепленные файлы |
MqlSqlDemo.zip (566.69 KB)
SetupHerdOfRobots.zip (6741.12 KB)
Сергей Ткаченко
Сергей Ткаченко | 14 июл 2018 в 11:13

К сожалению, по поводу Entity Framework ничего не могу сказать - я с ним никогда не работал. Да и вообще по относительно новым технологиям C# и .NET у меня опыта мало.

Если бы мне нужно было это сделать - я бы попробовал. Предположительно, будет работать. Особенно если не использовать продвинутые возможности внутри экспортируемых статических функций, а завернуть их в приватные функции каких-нибудь вспомогательных классов и применять уже там. У меня так в одном из проектов использовались классы из System.Collections.Generic.

Alexey Volchanskiy
Alexey Volchanskiy | 14 июл 2018 в 12:12
Сергей Ткаченко:

К сожалению, по поводу Entity Framework ничего не могу сказать - я с ним никогда не работал. Да и вообще по относительно новым технологиям C# и .NET у меня опыта мало.

Если бы мне нужно было это сделать - я бы попробовал. Предположительно, будет работать. Особенно если не использовать продвинутые возможности внутри экспортируемых статических функций, а завернуть их в приватные функции каких-нибудь вспомогательных классов и применять уже там. У меня так в одном из проектов использовались классы из System.Collections.Generic.

Ясно, опыт есть, при случае попробую. А вообще, вы замечали какие-либо минусы экспорта функций из DLL-файлов .NET в неуправляемый код? Я к тому, что Ренат Ф. сильно ругается на подобные вещи. Аргументы приводит самые общие, на уровне страшилок.

Вы замечали минусы? Например, снижение быстродействия, рост потребляемой памяти и т.д.

Сергей Ткаченко
Сергей Ткаченко | 14 июл 2018 в 15:21

О минусах тут можно говорить только в сравнении с чем-либо.

Если сравнивать советников, которые используют экспорт функций из DLL с теми советниками, которые не используют и работают на чистом MQL - тогда тут всё зависит от реализации конкретного советника и от того, какие задачи он выполняет. Если будут два советника, которые делают в точности одно и то же - тогда сложно сказать. Предполагаю, что быстрее будет тот, который использует DLL - из-за лучшей оптимизации кода компиляторами при сборке DLL. Но это только предположение, потому как напрямую я не сравнивал. Я обычно делал в DLL только то, что в MQL сделать сложнее или вообще нельзя (например, описанную в статье работу с БД). Поэтому мои советники, сделанные с обращением к DLL и без него, выполняли разные задачи.

У советников, использующих DLL, есть один недостаток - меньшая надёжность. Временами, хотя и редко, у них возникает иногда ошибка "Access violation at 0x08364576" (цифры адреса памяти разные). У роботов на чистом MQL такого нет. Тут, конечно, всё зависит от конкретной DLL - как она реализована, насколько сложная, все ли потенциально опасные с точки зрения ошибок памяти места проверены. Но у моих советников бывает ситуация, когда два-три месяца всё работает хорошо, а потом при перезапуске у одного из пяти запущенных на одном МТ советников вылетает эта ошибка в логе. В случае чистого MQL такого не бывает.

Если сравнивать экспорт функций из DLL на C# с экспортом из обычных DLL - скажем, на C++  - то здесь у каждого подхода свои преимущества и недостатки. Я делал другую DLL на С++ и Qt. Там тоже была работа с БД, но SQLite, а не SQL Server. Ошибки доступа к памяти тоже были, причём куда чаще, чем в DLL на .NET. Впрочем, если проект "вылизан", если везде, где нужно, идёт проверка указателей на null, освобождение  памяти и прочее в том же духе - тогда, может быть, наоборот будет более надёжно. Но в C# с этим как-то легче, там всё автоматически проверяется-освобождается. Отличий по производительности не замечал. Но, впрочем, мой проект на C++/Qt эксплуатировался мало.

Alexey Volchanskiy
Alexey Volchanskiy | 15 июл 2018 в 14:40
Сергей Ткаченко:

О минусах тут можно говорить только в сравнении с чем-либо.

Если сравнивать советников, которые используют экспорт функций из DLL с теми советниками, которые не используют и работают на чистом MQL - тогда тут всё зависит от реализации конкретного советника и от того, какие задачи он выполняет. Если будут два советника, которые делают в точности одно и то же - тогда сложно сказать. Предполагаю, что быстрее будет тот, который использует DLL - из-за лучшей оптимизации кода компиляторами при сборке DLL. Но это только предположение, потому как напрямую я не сравнивал. Я обычно делал в DLL только то, что в MQL сделать сложнее или вообще нельзя (например, описанную в статье работу с БД). Поэтому мои советники, сделанные с обращением к DLL и без него, выполняли разные задачи.

У советников, использующих DLL, есть один недостаток - меньшая надёжность. Временами, хотя и редко, у них возникает иногда ошибка "Access violation at 0x08364576" (цифры адреса памяти разные). У роботов на чистом MQL такого нет. Тут, конечно, всё зависит от конкретной DLL - как она реализована, насколько сложная, все ли потенциально опасные с точки зрения ошибок памяти места проверены. Но у моих советников бывает ситуация, когда два-три месяца всё работает хорошо, а потом при перезапуске у одного из пяти запущенных на одном МТ советников вылетает эта ошибка в логе. В случае чистого MQL такого не бывает.

Если сравнивать экспорт функций из DLL на C# с экспортом из обычных DLL - скажем, на C++  - то здесь у каждого подхода свои преимущества и недостатки. Я делал другую DLL на С++ и Qt. Там тоже была работа с БД, но SQLite, а не SQL Server. Ошибки доступа к памяти тоже были, причём куда чаще, чем в DLL на .NET. Впрочем, если проект "вылизан", если везде, где нужно, идёт проверка указателей на null, освобождение  памяти и прочее в том же духе - тогда, может быть, наоборот будет более надёжно. Но в C# с этим как-то легче, там всё автоматически проверяется-освобождается. Отличий по производительности не замечал. Но, впрочем, мой проект на C++/Qt эксплуатировался мало.

И еще вопрос, может быть занимались такими вещами. Можно ли из C# DLL запустить интерактивную панель управления или надо обязательно делать панель в виде отдельной программы и обеспечивать с DLL связь каким-либо из способов, например через Memory Mapping или WCF?

Я сейчас говорю про один локальный компьютер.

Сергей Ткаченко
Сергей Ткаченко | 16 июл 2018 в 10:38

Нет, с интерактивными панелями я не работал, к сожалению. Могу только предполагать. И я не совсем понимаю, что тут имеется в виду под "интерактивной панелью".

Нужно открывать какое-то из окон MetaTrader? Здесь я вообще не знаю, как это можно было бы сделать и можно ли вообще.

Нужно просто открывать какое-то окно и получать ввод от пользователя? Этим тоже не занимался, но думаю, что скорее всего это несложно. Сделать класс, в котором определяется обычное окно Windows Forms. В этом классе сделать статическую экспортируемую функцию, которая создаёт окно, показывает пользователю в диалоговом режиме, а потом освобождает. Должно сработать.

Социальный трейдинг. Можно ли прибыльный сигнал сделать еще лучше? Социальный трейдинг. Можно ли прибыльный сигнал сделать еще лучше?

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

Универсальный индикатор RSI для работы одновременно в двух направлениях Универсальный индикатор RSI для работы одновременно в двух направлениях

При разработке торговых алгоритмов мы часто сталкиваемся с проблемой: как определить, где начинается и заканчивается тренд/флэт? В этой статье попытаемся создать универсальный индикатор, в котором совместим сигналы для разных типов стратегий. Попробуем максимально упростить получение сигналов на торговые операции в эксперте. Приведем пример того, как объединить несколько разных индикаторов в одном.

Глубокие нейросети (Часть VIII). Повышение качества классификации bagging-ансамблей Глубокие нейросети (Часть VIII). Повышение качества классификации bagging-ансамблей

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

Графический конструктор стратегий. Создание торговых роботов без программирования Графический конструктор стратегий. Создание торговых роботов без программирования

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