База данных — это просто (Часть 1): Легковесный ORM-фреймворк для MQL5 на базе SQLite
Содержание
- Введение — упрощаем работу с базой данных
- Пошаговое создание ORM-фреймворка на языке MQL5
- Практическая реализация
- Полные примеры
- Заключение
Введение — упрощаем работу с базой данных
В мире алгоритмической торговли надежное управление данными имеет решающее значение. MQL5 предоставляет низкоуровневый API для работы с SQLite, который достаточно мощен, но требует ручной работы с SQL-запросами. В отличие от современных языков программирования, таких как C#, которые предлагают ORM-решения вроде ADO.NET или Entity Framework, в MQL5 на данный момент отсутствует пригодный для повторного использования и готовый к промышленной эксплуатации ORM-фреймворк. В данной статье представлен комплексный, легковесный, переносимый и профессиональный ORM-фреймворк – система SQLite ORM (объектно-реляционное отображение) для MQL5, которая добавляет в MetaTrader 5 возможности SQL-запросов и разработана специально для SQLite.
В отличие от традиционных подходов на основе SQL, эта система предоставляет fluent-интерфейс, который делает операции с базой данных интуитивно понятными и удобными для сопровождения. Сначала это может показаться утомительным или сложным, но создание системы управления базами данных облегчает работу и избавляет нас от длинных и утомительных команд.
Итак, мы будем шаг за шагом реализовывать эту систему управления базами данных в стиле ORM (объектно-реляционное отображение), что упростит нам работу с базой данных. В теории нет необходимости каждый раз копировать и вставлять базовые функции MQL5 для работы с SQL, такие как DatabaseOpen, DatabaseClose, DatabasePrepare, DatabaseFinalize и т. д., по всему коду. Таким образом, в данном случае нам нужен фреймворк, который объединяет все полезные методы, чтобы выполнять работу простым способом и удобно связывать данные базы с нужными переменными приложения. Затем в практической части мы реализуем собственную модель, которая отражает передачу данных между нужными нам переменными и файлом базы данных посредством объекта класса ORM. Наконец, мы тестируем реализованные классы в каждом разделе, чтобы проверить, соответствуют ли результаты нашим ожиданиям от фреймворка. В конце статьи приведены модуль фреймворка и тестовые файлы, позволяющие интегрировать его в любой проект, который нужен разработчику. На момент написания данной статьи в разделе "Статьи MQL5" не существует аналогичного ORM-фреймворка. Это первая в своем роде разработка для MQL5. Чтобы реализовать этот фреймворк, мы будем действовать пошагово.
Почему ORM в MQL5?
Типичный код работы с базой данных на MQL5:
- сильно зависит от SQL;
- имеет слабую обработку ошибок;
- не пригоден для повторного использования;
- не масштабируется для крупных советников.
Фреймворк решает следующие задачи:
- исключает SQL из бизнес-логики;
- обеспечивает строгую типизацию;
- позволяет применять подход Code First;
- повышает безопасность и сопровождаемость.
BaseModel → ORMField → DatabaseORM → SQLite API ↑ MacroModelОсновные компоненты:
- ORMField – метаданные столбца и привязка;
- BaseModel – базовый класс сущности;
- DatabaseORM – генерация и выполнение SQL-операторов (интерфейс в стиле ADO.NET);
- MacroModel – синтаксис Code First.
Пошаговое создание ORM-фреймворка на языке MQL5
1. Тип класса Dictionary – рефлексия сопоставления ключей
На первом этапе нам нужен класс, который хранит объекты в виде словаря; к нему можно обращаться по имени переменной всякий раз, когда потребуется соответствующий объект. Чтобы реализовать этот класс, нам необходимо унаследоваться от встроенного класса MQL5 CArrayObj и реализовать простую структуру словаря.
template<typename T> class CDictObj : public CArrayObj { public : CDictObj(void) {}; ~CDictObj(void) {Clear();}; T* operator[](const int index) const { return(At(index)); } T* operator[](const string param) const { for(int index=0; index<Total(); index++) { T* obj = At(index); if(obj.Name() == param) return obj; } return NULL; } };
Его нужно реализовать как обобщенный класс, чтобы мы могли привязать к нему любой класс и получить его из метода этого класса.
2. Сохранение свойств полей – определение класса для хранения информации о поле каждого столбца
Нам нужен еще один класс, который будет хранить поля, добавляемые в таблицу базы данных, в виде сопоставления столбцов, то есть хранить информацию об этих полях в формате входных параметров SQL-поля: имя / тип (Text | Integer | Real) / первичный ключ / автоинкремент / NOT NULL / значение по умолчанию. В этом классе хранятся важные свойства каждого поля столбца. Эти поля вместе с их именами хранятся в классе Dictionary, который мы недавно реализовали, чтобы их можно было использовать для передачи данных из этих полей в базу данных.
// Field Definition //+------------------------------------------------------------------+ class CORMField : public CObject { ... ... ... string Name() const { return m_name; } ENUM_FIELD_TYPE FieldType() const { return m_fieldType; } uint Flags() const { return m_flags; } // -------- Attributes -------- void PrimaryKey(bool v=true) { m_isPrimaryKey=v; } void AutoIncrement(bool v=true) { m_isAutoIncrement=v; } void Nullable(bool v=true) { m_isNullable=v; } template<typename T> void SetValue(T v) { if(m_value!=string(v)) m_value=string(v); } string GetValue() const { return m_value; } bool IsPrimaryKey() const { return m_isPrimaryKey; } bool IsAutoIncrement() const { return m_isAutoIncrement; } bool IsNullable() const { return m_isNullable; } ... ... ... }; //+------------------------------------------------------------------+
Этот класс предоставляет два важных метода для получения и установки значения нужного поля. Он используется для получения, установки или обновления значения нужного поля.
3. Создание базового класса модели для взаимодействия полей с базой данных – управление данными и SQL-моделями
CBaseModel служит основой для всех сущностей базы данных и обеспечивает автоматическое управление полями. Он обеспечивает строгую типизацию, автоматическое сопоставление и переиспользуемую логику для дочерних классов-наследников.
Класс CBaseModel хранит структуру таблицы с полями каждого столбца и управляет необработанными данными с помощью SQL-оператора. Фактически между базой данных и пользовательскими данными существует связь, работающая со структурой SQL-оператора, поэтому нет необходимости копировать SQL-оператор каждый раз, когда он нам нужен. Эта модель выполняет за нас SQL-операцию и обеспечивает выполнение запрошенных задач.
// Base Model Class class CBaseModel : public CObject { ... ... ... virtual void DefineFields(); void AddField(); string GetCreateTableQuery(); virtual string GetInsertQuery(); virtual string GetUpdateQuery(); virtual void LoadFromStatement(); string GetTableName(); int GetTableCount(); string GetPrimaryKeyName(); // --- Reflection-like API --- string Get(string field); void Set(string field,string value); // --------- Binding Interface (IMPORTANT) --------- virtual string OnGet(string field) { return ""; }; virtual void OnSet(string field,string value) {}; void PullFromModel(); void PushToModel(); ... ... ... };
Ниже приведены некоторые важные методы, которые она предоставляет:
| Методы | Описание |
|---|---|
| DefineFields | В дочернем классе мы можем использовать этот метод, чтобы автоматически добавить несколько полей, и тогда их не потребуется добавлять в других местах кода. |
| AddField | Добавляет поле столбца в таблицу. |
| GetCreateTableQuery | Здесь реализован метод создания таблицы. |
| GetInsertQuery | Метод формирования команды для вставки строки в базу данных является виртуальным и может быть переопределен в дочернем классе. |
| GetUpdateQuery | Метод формирования команды для обновления строки в базе данных является виртуальным и может быть переопределен в дочернем классе. |
| LoadFromStatement | В этом разделе выполняется привязка данных из базы данных к нужной нам переменной. Это виртуальный метод, который можно переопределить в дочернем классе. |
| GetTableName | Возвращает имя таблицы. |
| GetTableCount | Возвращает количество выбранных полей. |
| GetPrimaryKeyName | Возвращает имя поля, выбранного в качестве первичного ключа. |
| OnGet | Получает значение из интерфейса привязки в переменную; это абстрактный метод, который должен быть реализован в дочернем классе. |
| OnSet | Устанавливает значение из переменной в интерфейс привязки; это абстрактный метод, который должен быть реализован в дочернем классе. |
| PullFromModel | Автоматически извлекает значение из переменной. |
| PushToModel | Автоматически устанавливает значение переменной. |
Этот класс отвечает за передачу данных в БД: нам нужно лишь добавить нужные поля, чтобы передать их вместе с их значениями в базу данных без каких-либо дополнительных инструкций. Однако при наследовании от этого класса есть несколько ключевых методов, которые мы реализуем при создании нужной нам модели. Используя наследование от этого класса модели, мы можем определить собственную модель с геттерами и сеттерами. Переопределив три важных метода, мы реализуем собственную связь переменной с базой данных. В разделе "Примеры" вы увидите, как мы легко устанавливаем связь между нашей переменной и базой данных, используя наследование от этого класса модели.
4. API SQLite – класс, напрямую работающий с функциями SQLite
Существует еще один модуль, который напрямую взаимодействует с функциями SQLite, чтобы отправлять SQL-запросы в базу данных после установления соединения с ней. Класс CDatabase представляет собой простой модуль, который инкапсулирует функции SQLite и выполняет несколько необходимых полезных методов, таких как открытие, закрытие, чтение и удаление (Open, Close, Read, Delete).
// Database sqlite API class CDatabase { ... ... ... // ---------- Open / Close ---------- bool Open(string file, uint flags); void Close(); bool IsOpen(); int Prepare(string sql); void Finalize(int stmt); bool Execute(string sql); bool TransactionBegin(); bool TransactionCommit(); bool TransactionRollback(string sql); bool Read(int stmt); ... ... ... };
Объект этого класса содержит несколько наиболее полезных функций для прямого подключения к базе данных SQLite без дублирования базовых функций работы с базами данных MQL5 в других частях кода. Достаточно инициализировать объект этого класса, после чего с его помощью можно в любой момент напрямую подключиться к файлу нашей базы данных.
5. Основная архитектура – основа ORM для базы данных (создание базового ORM-класса)
Класс CDatabaseORM служит основой, обеспечивая управление подключениями, обработку транзакций и основные CRUD-операции (создание, чтение, обновление и удаление). Этот класс реализует самую простую и в то же время базовую архитектуру работы с базой данных: он принимает на вход объект модели, содержащий описание полей базы данных, и выполняет запрошенные задачи на основе формируемого объектом корректного SQL-запроса. Фактически, этот класс выполняет задачи, связанные с базой данных, включая CREATE/INSERT/UPDATE/DELETE:
// ORM core model class CDatabaseORM { ... ... ... bool Connect(); void Disconnect(); bool CreateTable(); bool Insert(); bool Update(); bool Delete(); bool Select(); bool SelectAll(); ... ... ... };
| Методы | Описание |
|---|---|
| Connect | Начинает подключение к базе данных. |
| Disconnect | Отключается от базы данных. |
| CreateTable | Создает таблицу по требованию. |
| Insert | Вставляет данные в таблицу. |
| Update | Обновляет данные в таблице. |
| Delete | Удаляет данные из таблицы. |
| Select | Ищет первый элемент, который требуется найти по условию WHERE. |
| SelectAll | Ищет все элементы, которые требуется найти по условию WHERE. |
Основные возможности:
- автоматическое управление подключениями – обеспечивает бесперебойную работу с подключениями к базе данных;
- поддержка транзакций – корректное открытие и закрытие соединений;
- применение SQL-операторов – полная поддержка CREATE, INSERT, UPDATE, DELETE и SELECT;
- пулинг соединений – эффективное управление ресурсами;
- обработка ошибок – подробное сообщение об ошибках и восстановление после них.
Этот класс берет на себя подключение к базе данных; теперь нам нужен механизм, который будет передавать данные, то есть связывать класс CDatabaseORM с объектом, в котором собраны данные для обновления или вставки в базу данных. Такой объект получает на вход данные из класса CBaseModel (или из модели, унаследованной от CBaseModel); через него мы передаем нужные команды, а CDatabaseORM выполняет эти команды.
Теперь перейдем к практическим примерам для каждой части класса, которую мы реализовали до сих пор.
Практическая реализация
1. Определение пользовательской модели торгового отчета
Давайте создадим комплексную модель для хранения результатов бэктеста:
class CTradeReportModel : public CBaseModel { private: long m_id; string m_strategy_name; datetime m_report_date; double m_total_net_profit; double m_profit_factor; double m_max_drawdown_relative; int m_total_trades; string m_parameters; public: CTradeReportModel(string table_name="trade_report") : CBaseModel(table_name) { m_id = 0; m_total_net_profit = 0; m_profit_factor = 0; m_max_drawdown_relative = 0; m_total_trades = 0; } CTradeReportModel(const CTradeReportModel& model) : CBaseModel(model) { } void DefineFields() override { AddField("id", FIELD_TYPE_INT, true, true, true); AddField("strategy_name", FIELD_TYPE_STRING, false, false, true); AddField("report_date", FIELD_TYPE_DATETIME, false, false, true); AddField("total_net_profit", FIELD_TYPE_DOUBLE); AddField("profit_factor", FIELD_TYPE_DOUBLE); AddField("max_drawdown_relative", FIELD_TYPE_DOUBLE); AddField("total_trades", FIELD_TYPE_INT); AddField("parameters", FIELD_TYPE_STRING); } string GetInsertQuery() override { string query = StringFormat("INSERT INTO %s (strategy_name, report_date, total_net_profit, profit_factor, " + "max_drawdown_relative, total_trades, parameters) " + "VALUES ('%s','%s', %d, %.2f, %.2f, %d, '%s')", GetTableName(), m_strategy_name, TimeToString(m_report_date), m_total_trades, m_profit_factor, m_max_drawdown_relative, m_total_trades, m_parameters); return query; } void LoadFromStatement(int statement) override { m_id = DatabaseColumnInt(statement, 0); m_strategy_name = DatabaseColumnText(statement, 1); m_report_date = (datetime)DatabaseColumnInt(statement, 2); m_total_net_profit = DatabaseColumnDouble(statement, 3); m_profit_factor = DatabaseColumnDouble(statement, 4); m_max_drawdown_relative = DatabaseColumnDouble(statement, 5); m_total_trades = DatabaseColumnInt(statement, 6); m_parameters = DatabaseColumnText(statement, 7); } // Setters void SetStrategyName(string name) { m_strategy_name = name; m_fields["strategy_name"].SetValue(name); } void SetTotalNetProfit(double profit) { m_total_net_profit = profit; m_fields["total_net_profit"].SetValue(profit);} void SetProfitFactor(double factor) { m_profit_factor = factor; m_fields["profit_factor"].SetValue(factor);} void SetMaxDrawdown(double max_dd) { m_max_drawdown_relative = max_dd; m_fields["max_drawdown_relative"].SetValue(max_dd);} void SetReportDate(datetime date) { m_report_date = date; m_fields["report_date"].SetValue(date);} void SetTotalTrades(int total) { m_total_trades = total; m_fields["total_trades"].SetValue(total);} void SetParameters(string param) { m_parameters = param; m_fields["parameters"].SetValue(param);} // Getters long GetId() const { return m_id; } string GetStrategyName() const { return m_strategy_name; } double GetTotalNetProfit() const { return m_total_net_profit; } double GetProfitFactor() const { return m_profit_factor; } double GetMaxDrawdown() const { return m_max_drawdown_relative; } datetime GetReportDate() const { return m_report_date; } int GetTotalTrades() const { return m_total_trades; } string GetParameters() const { return m_parameters; } };
Это пользовательский дочерний класс, наследующийся от CBaseModel, в котором мы предоставляем собственные геттеры/сеттеры. В методе "LoadFromStatement" мы фактически выполняем привязку наших переменных к данным базы данных. Когда мы хотим загрузить значения из базы данных, привязка выполняется этим методом, а затем мы вызываем геттер, чтобы получить значение из нужной переменной.
2. Использование интерфейса привязки на основе рефлексии
В другом примере мы можем привязать наши переменные для получения значений из базы данных и записи в нее с помощью интерфейса рефлексии, переопределив основные методы OnGet/OnSet, как показано ниже:
// binding model class TradeReportModel : public CBaseModel { public: int Id; string Symbol; double Lots; datetime OpenTime; TradeReportModel() { Table("TradeReport"); AddField(new CORMField("Id",FIELD_TYPE_INT,PRIMARY_KEY|AUTO_INCREMENT)); AddField(new CORMField("Symbol",FIELD_TYPE_STRING,REQUIRED)); AddField(new CORMField("Lots",FIELD_TYPE_DOUBLE)); AddField(new CORMField("OpenTime",FIELD_TYPE_DATETIME)); } // ---------- Binding ---------- string OnGet(string field) override { if(field=="Id") return (string)Id; if(field=="Symbol") return Symbol; if(field=="Lots") return DoubleToString(Lots); if(field=="OpenTime") return (string)OpenTime; return ""; } void OnSet(string field,string value) override { if(field=="Id") Id=(int)value; if(field=="Symbol") Symbol=value; if(field=="Lots") Lots=StringToDouble(value); if(field=="OpenTime") OpenTime=(datetime)value; } };
В этом типе привязки мы используем переопределенные функции, которые через рефлексию обращаются к привязанным переменным и перенаправляют обращения между нашими данными и полями.
3. Использование возможностей макросов – Code First-модели на макросах
Существует еще один способ реализовать наш собственный пользовательский класс в кратчайшие сроки – с помощью макросов. Итак, пользовательский класс с несколькими переменными и соответствующими геттерами/сеттерами, а также привязкой переменных к базе данных:
//--------DB class start //-- class start DB_CLASS_BEGIN(CMyDBClassModel) //-- defining the getter/setter members DB_DEFINE_FIELD_INT(id) DB_DEFINE_FIELD_STRING(strategy_name) DB_DEFINE_FIELD_DATETIME(report_date) DB_DEFINE_FIELD_DOUBLE(total_net_profit) DB_DEFINE_FIELD_DOUBLE(profit_factor) DB_DEFINE_FIELD_DOUBLE(max_drawdown_relative) DB_DEFINE_FIELD_INT(total_trades) DB_DEFINE_FIELD_STRING(parameters) //-- adding the members DB_ADD_BEGIN DB_ADD_FIELD_INT(id,PRIMARY_KEY | AUTO_INCREMENT | REQUIRED) DB_ADD_FIELD_STRING(strategy_name,REQUIRED) DB_ADD_FIELD_DATETIME(report_date,REQUIRED) DB_ADD_FIELD_DOUBLE(total_net_profit,FIELD_NONE) DB_ADD_FIELD_DOUBLE(profit_factor,FIELD_NONE) DB_ADD_FIELD_DOUBLE(max_drawdown_relative,FIELD_NONE) DB_ADD_FIELD_INT(total_trades,FIELD_NONE) DB_ADD_FIELD_STRING(parameters,FIELD_NONE) DB_ADD_END //-- binding the members DB_BIND_BEGIN DB_BIND_FIELD_INT(id,0) DB_BIND_FIELD_STRING(strategy_name,1) DB_BIND_FIELD_DATETIME(report_date,2) DB_BIND_FIELD_DOUBLE(total_net_profit,3) DB_BIND_FIELD_DOUBLE(profit_factor,4) DB_BIND_FIELD_DOUBLE(max_drawdown_relative,5) DB_BIND_FIELD_INT(total_trades,6) DB_BIND_FIELD_STRING(parameters,7) DB_BIND_END //-- class end DB_CLASS_END
Здесь определяются и привязываются поля типа Integer, Double, String, Datetime и Boolean, а также создается пользовательский класс "CMyDBClassModel". Определив объект этого класса, мы получаем собственную пользовательскую модель, унаследованную от CBaseModel, что позволяет нам передать структуру полей таблицы нашему объекту базы данных CDatabaseORM, чтобы к этому объекту можно было применить нужную нам команду. В следующем разделе мы перейдем к тестовому примеру и воспользуемся этими классами, которые мы реализовали, чтобы убедиться, насколько просто работать с базой данных.
Полные примеры
Пример простого использования классов с последующей передачей модели объекту ORM и ее вызовом при заполнении базы данных результатами бэктеста. Ниже приведен подробный пример, в котором создается база данных и заполняется примерными данными бэктеста. (файл TestDatabase.mq5):
1. С использованием объекта CBaseModel
bool InitializeDatabase1() { Print("=== Backtest Data Generator by Native Base ==="); Print("Initializing database..."); if(!m_database.Connect()) { Print("Failed to connect to database"); return false; } // Create tables report_model.AddField("id", FIELD_TYPE_INT, true, true, true); report_model.AddField("strategy_name", FIELD_TYPE_STRING, false, false, true); report_model.AddField("report_date", FIELD_TYPE_DATETIME, false, false, true); report_model.AddField("total_net_profit", FIELD_TYPE_DOUBLE); report_model.AddField("profit_factor", FIELD_TYPE_DOUBLE); report_model.AddField("max_drawdown_relative", FIELD_TYPE_DOUBLE); report_model.AddField("total_trades", FIELD_TYPE_INT); report_model.AddField("parameters", FIELD_TYPE_STRING); if(!m_database.CreateTable(report_model)) { Print("Failed to create table"); return false; } Print("Database initialized successfully"); return true; }
Здесь, после подключения к базе данных, мы инициализируем нужную нам модель с помощью объекта класса CBaseModel, используя метод AddField. Затем мы создаем таблицу, передавая тот же объект модели в базу данных. Затем мы задаем примерные значения, которые необходимо передать в базу данных:
// Sample strategy configurations string strategies[] = { "MA_Crossover_10_20", "RSI_Strategy_14_30_70", "Bollinger_Bands_20_2", "MACD_Strategy_12_26_9", "Stochastic_14_3_3", "Parabolic_SAR_002_02", "Ichimoku_Cloud", "ADX_Strategy_14", "Price_Action_Breakout", "Volume_Weighted_MA" }; string symbols[] = {"EURUSD", "GBPUSD", "USDJPY", "AUDUSD", "XAUUSD"}; string timeframes[] = {"H1", "H4", "D1", "W1"}; int totalRecords = 0; for(int i = 0; i < ArraySize(strategies); i++) { for(int j = 0; j < ArraySize(symbols); j++) { for(int k = 0; k < ArraySize(timeframes); k++) { // Generate realistic backtest results report_model["strategy_name"].SetValue(strategies[i] + "_" + symbols[j] + "_" + timeframes[k]); report_model["report_date"].SetValue(TimeCurrent() - (MathRand() % 2592000)); // Random date in last month report_model["total_net_profit"].SetValue(GenerateRealisticProfit()); report_model["profit_factor"].SetValue(1.0 + (MathRand() % 200) / 100.0); // 1.0 to 3.0 report_model["max_drawdown_relative"].SetValue(5.0 + (MathRand() % 250) / 10.0); // 5% to 30% report_model["total_trades"].SetValue(50 + (MathRand() % 450)); // 50 to 500 trades string parameters = StringFormat("Symbol=%s, Timeframe=%s, Strategy=%s", symbols[j], timeframes[k], strategies[i]); report_model["parameters"].SetValue(parameters); if(m_database.Insert(report_model)) { totalRecords++; } // Add some variation Sleep(10); } } }
Здесь мы создали примерные значения и передали их в модель через словарный доступ к полям, а затем с помощью той же модели передали эти значения в базу данных, чтобы выполнить SQL-оператор INSERT методом Insert объекта модели базы данных. На изображении ниже вы можете увидеть эти данные, сохраненные в базе данных, после открытия файла базы данных в MetaEditor.
Вывод:

Это сообщение в отчете об инициализации базы данных с помощью примера кода. Ниже в сообщениях отчета последовательно выводятся три примера кода для создания базы данных с помощью каждой модели.
Вывод:

Далее с помощью этого кода мы можем легко выбрать одну из нужных нам строк:
... ... ... // Select example if(db.Select(report_model)) { PrintFormat("Found record : %s", report_model["strategy_name"].GetValue()); } // SelectAll example CArrayObj found_array; int items = db.SelectAll(found_array,report_model,"profit_factor > 1.0"); Print("items : ", items); if(items>0) { for(int i=0; i<found_array.Total(); i++) { CBaseModel* _temp = found_array.At(i); if(_temp!=NULL) PrintFormat("Found record : %s", _temp["strategy_name"].GetValue()); } } ... ... ...
Как видите, чтобы получить первое значение из базы данных, мы вызвали метод Select объекта CDataBaseORM, который выполняет SQL-команду SELECT без условия WHERE. В следующей части нам нужно было найти и вернуть несколько значений, поэтому мы вызвали метод SelectAll с условием WHERE "profit_factor > 1.0", и, как видите, эта команда была выполнена корректно и вернула нужные записи.
Вывод:

2. С помощью пользовательского объекта CTradeReportModel
Здесь речь идет об использовании реализованного нами пользовательского класса CTradeReportModel:
... ... ... bool InitializeDatabase2() { Print("=== Backtest Data Generator by Inherited Base ==="); Print("Initializing database..."); if(!m_database.Connect()) { Print("Failed to connect to database"); return false; } // Create tables trade_model.DefineFields(); if(!m_database.CreateTable(trade_model)) { Print("Failed to create table"); return false; } Print("Database initialized successfully"); return true; } ... ... ...
Здесь, после подключения к нужной нам базе данных, мы вызываем метод DefineFields, который, как и в предыдущем примере, автоматически выполнит вызов AddFields, добавленный нами в этом методе. Затем мы создаем таблицу с помощью ORM-объекта базы данных и объекта пользовательской модели.
... ... ... // Generate realistic backtest results trade_model.SetStrategyName(strategies[i] + "_" + symbols[j] + "_" + timeframes[k]); trade_model.SetReportDate(TimeCurrent() - (MathRand() % 2592000)); // Random date in last month trade_model.SetTotalNetProfit(GenerateRealisticProfit()); trade_model.SetProfitFactor(1.0 + (MathRand() % 200) / 100.0); // 1.0 to 3.0 trade_model.SetMaxDrawdown(5.0 + (MathRand() % 250) / 10.0); // 5% to 30% trade_model.SetTotalTrades(50 + (MathRand() % 450)); // 50 to 500 trades string parameters = StringFormat("Symbol=%s, Timeframe=%s, Strategy=%s", symbols[j], timeframes[k], strategies[i]); trade_model.SetParameters(parameters); if(m_database.Insert(trade_model)) ... ... ...
Здесь мы действуем так же, как в предыдущем примере, но с небольшим отличием: помимо возможности использовать словарный доступ к полям, мы также можем использовать доступ через геттеры и сеттеры с помощью реализованных нами пользовательских методов. Далее, как и в предыдущем примере, мы можем извлечь нужные нам значения из базы данных с условием WHERE следующим образом:
... ... ... // Select example CTradeReportModel found_by_select; if(db.Select(found_by_select)) { PrintFormat("Found record : %s", found_by_select.GetStrategyName()); } // SelectAll example CTradeReportModel found_by_selectAll; CArrayObj found_array; if(db.SelectAll(found_array,found_by_selectAll,"profit_factor > 1.0")>0) { for(int i=0; i<found_array.Total(); i++) { CTradeReportModel* _temp = found_array.At(i); PrintFormat("Found record : %s", _temp.GetStrategyName()); } } ... ... ...
Здесь класс CArrayObj возвращает объект нашего пользовательского класса модели – класса CTradeReportModel, – и мы вызываем определенный нами геттер для получения значений. Как видно, метод SelectAll также корректно возвращает значения, что подтверждается выводом объектов на экран.
Вывод:

3. Использование возможностей макрообъекта
В этой части мы можем без труда выполнить те же действия, что и в только что приведенном примере, с той разницей, что будем использовать пользовательский класс, созданный с помощью макроса:
... ... ... bool InitializeDatabase3() { Print("=== Backtest Data Generator by Macro Base ==="); Print("Initializing database..."); if(!m_database.Connect()) { Print("Failed to connect to database"); return false; } // Create tables macro_model.DefineFields(); if(!m_database.CreateTable(macro_model)) { Print("Failed to create table"); return false; } Print("Database initialized successfully"); return true; } ... ... ...
Здесь мы делаем то же, что и ранее, используя пользовательский класс, созданный макросом, и передаем примерные значения в базу данных:
... ... ... // Generate realistic backtest results macro_model.Set_strategy_name(strategies[i] + "_" + symbols[j] + "_" + timeframes[k]); macro_model.Set_report_date(TimeCurrent() - (MathRand() % 2592000)); // Random date in last month macro_model.Set_total_net_profit(GenerateRealisticProfit()); macro_model.Set_profit_factor(1.0 + (MathRand() % 200) / 100.0); // 1.0 to 3.0 macro_model.Set_max_drawdown_relative(5.0 + (MathRand() % 250) / 10.0); // 5% to 30% macro_model.Set_total_trades(50 + (MathRand() % 450)); // 50 to 500 trades string parameters = StringFormat("Symbol=%s, Timeframe=%s, Strategy=%s", symbols[j], timeframes[k], strategies[i]); macro_model.Set_parameters(parameters); if(m_database.Insert(macro_model)) ... ... ...
Как видно, автоматические геттеры/сеттеры реализуются с помощью определения макроса. Затем, как и в предыдущем примере, чтобы получить нужные значения по условию WHERE, мы вызываем метод SelectAll из базы данных:
... ... ... // Select example CMyDBClassModel found_by_select; if(db.Select(found_by_select)) { PrintFormat("Found record : %s", found_by_select.Get_strategy_name()); } // SelectAll example CMyDBClassModel found_by_selectAll; CArrayObj found_array; if(db.SelectAll(found_array,found_by_selectAll,"profit_factor > 1.0")>0) { for(int i=0; i<found_array.Total(); i++) { CMyDBClassModel* _temp = found_array.At(i); PrintFormat("Found record : %s", _temp.Get_strategy_name()); } } ... ... ...
Здесь в классе модели возвращается объект класса CMyDBClassModel, и мы выводим его значение с помощью геттера, полученного методом SelectAll.
Вывод:

4. Интерфейс привязки рефлексии
В другом примере, чтобы привязать наши переменные с помощью рефлексии, которую мы недавно реализовали в виде переопределенных методов OnGet/OnSet, мы поступаем следующим образом:
... ... ... // creating fields CORMField *id=new CORMField("id",FIELD_TYPE_INT); id.PrimaryKey(true); id.AutoIncrement(true); CORMField *name=new CORMField("name",FIELD_TYPE_STRING); name.SetValue("Alice"); // adding fields to model model.AddField(id); model.AddField(name); // spending the model fields to ORM database orm.CreateTable(model); orm.Insert(model); ... ... ...
Сначала мы создали поля столбцов и добавили их в модель, созданную на основе класса CBaseModel, после чего она будет передана ORM для заполнения нужной базы данных. Другим способом мы использовали модель, унаследованную от нашей сущности CBaseModel, которая также реализует API рефлексии:
... ... ... // creating ORM DB CDatabaseORM db; db.Connect("trade.db", db_flags); // definging the inherited model TradeReportModel report; report.Symbol = "EURUSD"; report.Lots = 0.10; report.OpenTime = TimeCurrent(); // spending the custom model to ORM DB db.CreateTable(report); db.Insert(report); // Retrieving data from DB by custom model TradeReportModel loaded; if(db.Select(loaded,"Symbol='EURUSD'")) { Print("Loaded: ",loaded.Symbol," ",loaded.Lots); } // Updating data loaded.Lots = 0.20; db.Update(loaded); // Soft delete db.Delete(loaded); ... ... ...
С помощью этих четырех простых способов мы можем легко подключиться к базе данных, обойдясь без длинных и утомительных SQL-команд.
Преимущества, варианты применения и основные достоинства:
- Типобезопасность: проверка имен и типов полей на этапе компиляции
- Производительность: эффективная генерация и выполнение SQL-операторов
- Гибкость: поддержка сложных запросов и транзакций
Заключение
Этот мощный ORM-фреймворк упрощает разработку баз данных и привносит в MQL5 современные возможности управления базами данных в стиле ORM из C#, создавая надежную основу для хранения и анализа данных торговых стратегий. Независимо от того, создаете ли вы комплексную систему бэктестов, отслеживаете результаты реальной торговли или проводите исследование торговых стратегий, этот ORM-фреймворк предоставляет инструменты, необходимые для эффективного управления данными в MetaTrader 5; также мы узнали, как легко подключаться к базе данных, отправлять в нее данные и извлекать их. Полный исходный код доступен в прилагаемых заголовочных файлах; он подходит для профессиональных и крупномасштабных проектов и готов к интеграции в ваши проекты на MQL5. В следующей таблице описаны все файлы исходного кода, прилагаемые к статье.
| Имя файла | Описание |
|---|---|
| BaseModel v1.0.mqh BaseModel v2.0.mqh | Файл, содержащий базовые классы моделей для взаимодействия данных с базой данных |
| DatabaseORM v1.0.mqh DatabaseORM v2.0.mqh | Файл, содержащий ORM-фреймворк, который принимает объект базовой модели для выполнения SQL-запросов |
| Database.mqh | Файл, содержащий инкапсулированные базовые функции MQL5 для работы с SQL |
| MacroModel.mqh | Файл, содержащий определения макросов для быстрого создания пользовательского базового класса |
| MyDBClassModel v1.0.mqh | Файл, содержащий пример кода пользовательского базового класса, созданного с помощью макроса |
| TradeReportModel v1.0.mqh TradeReportModel v2.0.mqh | Файл, содержащий пример кода класса, унаследованного от CBaseModel |
| ORMError.mqh | Файл, содержащий определения ошибок базы данных и SQL |
| ORMField.mqh | Файл, содержащий коллекцию полей столбцов базы данных |
| TestDatabase.mq5 TestDatabase v2.mq5 | Файл, содержащий примеры использования каждой из моделей: модель CBase | унаследованная модель | модель на основе макросов | модель с привязкой через рефлексию |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20654
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Машинное обучение и Data Science (Часть 47): Прогнозирование рынка с помощью модели DeepAR на Python
Эволюция городских советов — City Councils Evolution (CCE)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования