English Deutsch
preview
データベースは簡単(第1回):SQLiteを用いたMQL5向け軽量ORMフレームワーク

データベースは簡単(第1回):SQLiteを用いたMQL5向け軽量ORMフレームワーク

MetaTrader 5 |
25 0
Hans Alexander Nolawon Djurberg
Hans Alexander Nolawon Djurberg


目次



はじめに:データベース操作の簡素化

アルゴリズム取引の世界では、堅牢なデータ管理が非常に重要です。MQL5はSQLite用の低レベルなデータベースAPIを提供しており強力ではありますが、手動でSQLを扱う必要があります。C#のADO.NETやEntity FrameworkのようなORMソリューションとは異なり、MQL5には再利用可能で実用的なORMフレームワークが現時点では存在していません。本記事では、MetaTrader 5向けに設計された軽量でポータブルかつプロフェッショナルなORM (Object-Relational Mapping)システムを紹介します。このシステムはSQLiteを対象としており、SQLクエリ操作をより扱いやすくするために設計されています。

従来のSQLベースの方法とは異なり、このシステムはフルエントインターフェースを提供し、データベース操作を直感的かつ保守しやすくします。 最初は面倒で難しく感じるかもしれませんが、データベース管理システムを構築することで作業は大幅に簡素化され、長く煩雑なコマンドから解放されます。 

この考え方に基づき、ORMスタイルのデータベース管理システムを段階的に実装していきます。これにより、データベース操作が容易になります。DatabaseOpen、 DatabaseClose、 DatabasePrepare、 DatabaseFinalizeなどのMQL5の基本SQL関数を毎回コードにコピー&ペーストする必要がなくなります。その代わりに、データベースと変数間でデータをスムーズに受け渡すための有用なメソッドを集約したフレームワークが必要になります。実装セクションでは、ORMクラスオブジェクトを通してデータベースファイルと変数の間でデータを対応付ける独自のモデルを作成します。その後、各セクションで実装したクラスをテストし、期待通りの結果が得られるかを確認します。記事の最後には、任意のプロジェクトに統合できるフレームワークモジュールとテストファイルが用意されています。執筆時点では、MQL5の記事セクションに同様のORMフレームワークは存在しておらず、本記事が初の試みとなります。このフレームワークの実装は、以下のステップで進めていきます。


MQL5でORMを採用する理由は?

典型的なMQL5のデータベースコードは以下の特徴があります。

  • SQL中心で記述量が多い 
  • エラー処理が弱い
  • 再利用できない
  • 大規模EAにスケールしにくい

このフレームワークの目的

  • ビジネスロジックからSQLを排除する
  • 強力な型付けを提供する
  • コードファースト開発を可能にする
  • 安全性と保守性を向上させる

アーキテクチャの概要
BaseModel → ORMField → DatabaseORM → SQLite API
  ↑ MacroModel

主要コンポーネント

  • ORMField: 列のメタデータおよびバインディング
  • BaseModel: エンティティ基底クラス
  • DatabaseORM: SQLの生成と実行(ADO.NETスタイルのインターフェース)
  • MacroModel: コードファースト構文


MQL5におけるORMフレームワークの段階的構築

1. 辞書クラス型:キーとオブジェクトの対応付け

最初のステップでは、辞書型としてオブジェクトを保存するクラスが必要です。このクラスを使うことで、オブジェクトに付けた変数名をキーとして、必要なときにそのオブジェクトへアクセスできるようになります。このクラスを実装するためには、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; }

   ...
   ...
   ...

  };
//+------------------------------------------------------------------+

このクラスは、目的のフィールドの値を取得および設定するための2つの重要なメソッドを提供します。これにより、特定のフィールドの値を取得したり、設定したり、あるいは更新したりすることが可能になります。

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既存のレコードを更新するためのSQLコマンドであり、こちらも仮想メソッドとして子クラスでオーバーライドできます。
LoadFromStatementクエリ結果を変数へバインドする処理をおこなう部分であり、これも仮想メソッドとして子クラスでオーバーライド可能です。
GetTableNameテーブル名を返します。
GetTableCount選択されているフィールドの数を返します。
GetPrimaryKeyName主キーとして設定されているフィールド名を返します。
OnGetバインディングインターフェースから変数へ値を取得するための抽象メソッドであり、子クラスで必ず実装する必要があります。
OnSet変数からバインディングインターフェースへ値を設定するための抽象メソッドであり、こちらも子クラスで実装されます。
PullFromModel変数からの値を自動的に取得します。
PushToModel変数からの値を自動的に設定します。

このクラスは、データベースへのデータ転送処理そのものを抽象化して表現しています。開発者は必要なフィールドを追加するだけで、それらの値を特別な追加処理なしにデータベースへ転送できるようになります。ただし、このクラスを継承して利用する場合には、いくつかの重要なメソッドを自分のモデルに合わせて実装する必要があります。このモデルクラスからの継承を用いることで、独自のゲッター・セッターモデルを定義することができます。特に3つの重要なメソッドをオーバーライドすることで、変数とデータベースの接続方法を自由に定義することが可能になります。例のセクションでは、このモデルクラスを継承することで、変数とデータベースの間の接続をどのように簡単に確立できるかを確認することができます。

4. SQLite API: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 (Create/Read/Update/Delete)操作を提供します。このクラスは最も基本的であると同時に中核的なデータベースアーキテクチャです。このクラスは、モデルオブジェクトを受け取り、そのモデルはデータベースのフィールドを保持しており、入力として使用されます。そして、このクラスは適切な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テーブルからデータを削除します。
SelectWHERE条件に基づいて、最初に見つかった項目を検索します。
SelectAllWHERE条件に基づいて、検索対象となるすべてのアイテムを検索します。

主な特徴は以下の通りです。

  • 自動接続管理:データベース接続をシームレスに処理します。
  • トランザクションサポート:接続および切断に関する完全な対応をおこないます。
  • 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. マクロの力を活用する:コードファーストのマクロモデル

マクロの力を活用することで、独自のカスタムクラスを最短時間で実装する別の方法があります。複数の変数とそれに対応するゲッター/セッターメソッドを持つカスタムクラスを作成し、さらにその変数をデータベースにバインドします。

//--------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;
     }

ここでは、データベースに接続した後、AddFieldメソッドを使用して、CBaseModelクラスのオブジェクトから目的のモデルを初期化します。そして、同じモデルオブジェクトをデータベースに提示することでテーブルを作成します。その後、サンプル値を設定して、​データベースに転送します。

// 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);
              }
           }
        }

ここでは、サンプルの値を作成し、それらの値を辞書方式を用いてモデルに渡しています。その後、同じモデルを使用して値をデータベースに渡し、データベースモデルオブジェクトのInsertメソッドによってINSERT文を実行しています。下の画像では、MetaEditorでデータベースファイルを開いた際に、これらのデータがそれぞれデータベースに保存されている様子を確認できます。

出力

サンプルコードによって作成されたデータベース

以下は、サンプルコードを使用してデータベースを初期化した際のレポートメッセージです。各モデルごとにデータベースを作成するためのサンプルコードが3つあり、それぞれ以下のレポートメッセージに表示されています。

出力

サンプルコードによるデータベース初期化のレポートメッセージ

次に、このコードを使用することで、必要な行を簡単に選択できます。

...
...
...
// 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());
        }

     }
...
...
...

ご覧のとおり、データベースから最初の値を取得するために、CDataBaseORMオブジェクトのSQLコマンドからWHERE条件なしでSELECTを提供するSelectメソッドを呼び出しました。そして次の部分では、複数の値を検索して返すようにしました。 ​そこで、私たちはWHERE条件「profit_factor > 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データベースに渡して、目的のデータベースにデータが格納されます。別の方法として、リフレクションAPIも実行するCBaseModelエンティティから継承したモデルを使用しました。

...
...
...

   // 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);

...
...
...

これら4つの簡単な方法を使えば、長くて面倒なSQLコマンドを使わずに、簡単にデータベースに接続できます。

メリット、ユースケース、主な利点

  • 型安全性:コンパイル時のフィールド名と型のチェック
  • パフォーマンス:効率的なSQL生成と実行
  • 柔軟性:複雑なクエリ、トランザクションのサポート



結論

この強力なORMフレームワークシステムは、データベース開発を簡素化し、現代的なC#のORMコンセプトによるデータベース管理機能をMQL5にもたらします。そして、取引戦略データの保存および分析のための堅牢な基盤を提供します。包括的なバックテストシステムの構築、リアルタイム取引パフォーマンスの追跡、あるいは戦略研究の実施など、どのような用途においても、このORMはMetaTrader 5における効果的なデータ管理のためのツールを提供します。また、データベースへの接続、データの送信および取得を容易におこなう方法についても学びました。完全なソースコードは付属のヘッダファイルとして提供されており、プロフェッショナルかつ大規模なプロジェクトにも適しており、MQL5プロジェクトへすぐに統合できるようになっています。次の表では、この記事に付随するすべてのソースコードファイルについて説明します。

ファイル名詳細
BaseModel v1.0.mqh
BaseModel v2.0.mqh
データとデータベース間の通信に使用する基底クラスモデルを含むファイル 
DatabaseORM v1.0.mqh
DatabaseORM v2.0.mqh
SQLクエリコマンドを実行するための基本モデルオブジェクトを取得するORMフレームワークを含むファイル
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 5用シグマスコアインジケーター:単純な統計的異常検出器 MetaTrader 5用シグマスコアインジケーター:単純な統計的異常検出器
MetaTrader 5用の実践的なSigma Score(シグマスコア)インジケーターをゼロから構築し、その指標が本質的に何を測定しているのかを理解します。シグマスコアとは、対数収益率のz得点(直近の値動きが過去の平均から標準偏差でどれだけ乖離しているか)を表すものです。OnInit()、OnCalculate()、OnDeinit()の各コードブロックを一つずつ丁寧に解説しながら実装を進めます。さらに、±2といった閾値の解釈方法や、このシグマスコアを「市場ストレスメーター」として活用し、平均回帰戦略およびモメンタム戦略の双方に応用する方法についても説明します。
ラリー・ウィリアムズの『市場の秘密』(第4回):MQL5における短期的スイングハイとスイングローの自動化 ラリー・ウィリアムズの『市場の秘密』(第4回):MQL5における短期的スイングハイとスイングローの自動化
MQL5を使って、ラリー・ウィリアムズの短期スイングパターンの自動化を習得していきます。このガイドでは、非ランダムな市場構造を活用する、完全に設定可能なエキスパートアドバイザー(EA)を開発します。堅牢なリスク管理と柔軟なエグジットロジックの統合方法も解説し、システマティックな戦略開発とバックテストのための確かな基盤を提供します。
ラリー・ウィリアムズの『市場の秘密』(第5回):MQL5におけるボラティリティブレイクアウト戦略の自動化 ラリー・ウィリアムズの『市場の秘密』(第5回):MQL5におけるボラティリティブレイクアウト戦略の自動化
ラリー・ウィリアムズのボラティリティブレイクアウト戦略をMQL5で自動化する方法を、実践的なステップで解説します。日次のレンジ拡張の計算方法、買いと売りレベルの導出、値幅に基づくストップロスとリスクリワードに基づく利益目標によるリスク管理、そしてMetaTrader 5で動作するプロフェッショナルなエキスパートアドバイザー(EA)の構造まで学ぶことができます。これは、ラリー・ウィリアムズの市場概念を完全にテスト可能かつ実運用できる自動売買システムへと変換したいトレーダーや開発者向けに設計されています。
Python-MetaTrader 5ストラテジーテスター(第2回):シミュレーターにおけるバー、ティック、組み込み関数のオーバーロード処理 Python-MetaTrader 5ストラテジーテスター(第2回):シミュレーターにおけるバー、ティック、組み込み関数のオーバーロード処理
本記事では、Python-MetaTrader 5モジュールが提供する関数に類似した機能を紹介し、使い慣れたインターフェースを備えた、バーおよびティックを内部で独自に処理するシミュレーターを提供します。