English Deutsch 日本語
preview
База данных — это просто (Часть 1): Легковесный ORM-фреймворк для MQL5 на базе SQLite

База данных — это просто (Часть 1): Легковесный ORM-фреймворк для MQL5 на базе SQLite

MetaTrader 5Примеры |
43 0
Hans Alexander Nolawon Djurberg
Hans Alexander Nolawon Djurberg


Содержание



Введение — упрощаем работу с базой данных

В мире алгоритмической торговли надежное управление данными имеет решающее значение. 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 &gt; 1.0", и, как видите, эта команда была выполнена корректно и вернула нужные записи.

Вывод:

Запрос, выполняемый SelectAll

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 также корректно возвращает значения, что подтверждается выводом объектов на экран.

Вывод:

Запрос, выполняемый 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.

Вывод:

Запрос, выполняемый 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

Прикрепленные файлы |
DatabaseORM.zip (19.65 KB)
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Машинное обучение и Data Science (Часть 47): Прогнозирование рынка с помощью модели DeepAR на Python Машинное обучение и Data Science (Часть 47): Прогнозирование рынка с помощью модели DeepAR на Python
В этой статье мы попробуем спрогнозировать движения рынка с помощью достаточно хорошей модели прогнозирования временных рядов под названием DeepAR. Модель представляет собой сочетание глубоких нейронных сетей и авторегрессионных свойств, характерных для таких моделей, как ARIMA и VAR (векторная авторегрессионная модель).
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Эволюция городских советов — City Councils Evolution (CCE) Эволюция городских советов — City Councils Evolution (CCE)
Выполняем каноническую реализацию CCE в рамках C_AO, воспроизводим тесты и показываем ключевой дефект: аддитивный покоординатный оператор тянет решения к нулю и деградирует на смещенных диапазонах. Предлагаем простой контрольный тест со сдвигом области поиска и модификацию на основе разностей, применяемую по всем координатам. В результате — исходники MQL5, методика проверки и улучшенная версия алгоритма.