見出し画像

【FX/EA】DLLを使用してMT4/MT5とWebサーバで通信する仕組みを実装する

MetaTraderって限界ありませんか?
AIオートトレードのEAを作成する中で、機械学習ライブラリ豊富なPython使いたいなあと思い、、

以下のようなシステムを構築しました。

MetaTrader上で得られるチャート情報を解析し、売買タイミングの指示を出すサーバを構築しています。
このサーバ上には、過去データを使用して学習したAIが搭載されており、チャート情報から、優位性の高い売買タイミングを導き出すことができます。

この時、MetaTraderと解析サーバの通信を仲介するのがDLLとなっています。DLLを使えるとEA開発の幅がグッと広がります。

本記事では、これらの基盤部分を備忘録がてら残しておこうかと思います!

※注意
チャート情報の送信や解析に関わる具体的なコードは載せておりません。
各コンポーネントにおける、基礎的な土台の部分のみを紹介しております。


DLLを使用してMT4/MT5とWebサーバで通信する仕組みを実装する


必要なもの

DLLとは

まず、DLLとは何なのかについて軽く解説していきます!
(読み飛ばしていただいても簡易的な実装においては問題ないかと思います。)

下記のような形式で記載していくので、「要約」部分だけ読むのでも十分かと思います。それでは解説していきます。

■ 要約

詳細説明。これは詳細説明です。


■ DLLは、複数のアプリケーションで共有して利用することができます

DLL(Dynamic Link Library)は、Windowsのオペレーティングシステムにおいて、共有可能なコードやリソースを提供する重要なコンポーネントです。

■ 一度DLLを作れば、使い回しができます

DLLは、機能を実装したコード(関数やクラス、変数など)が含まれているファイルです。このDLLを参照することで、他のプログラムからそのコードを呼び出して利用することができます。

■ DLLの利点は3つ。
①コードの再利用
②メモリの節約
③機能追加とメンテナンスが楽

コードの再利用: DLLを使うことで、一度書いたコードを複数のアプリケーションで再利用できるようになります。これにより、開発の手間が省け、コードの整合性も保たれます。コードを重複して書く必要がなくなることを意味するので、開発が効率化されます。
メモリ節約: 同じDLLを参照する複数のアプリが同時に動作している場合、そのDLLのコードはメモリ上に1度だけロードされます。これにより、システム全体のメモリ使用量を抑えることができます。
機能の追加とバグ修正: DLLを使えば、アプリケーションに新しい機能を追加したり、既存の不具合を修正したりするのが容易になります。新しい、または修正されたDLLを配布するだけで、それを参照するすべてのアプリが更新されます。

以上がDLLの説明となります。開発者からするととても嬉しい特徴ばかりですね。

解析サーバの実装

解析サーバを実装していきます。今回は簡易的な実装とします。
具体的には、リクエストが飛んで来たら、以下の3つのシグナルをランダムに返却します。

  • Wait:何もしない

  • Buy:買いシグナル

  • Sell:売りシグナル

Python環境のセットアップ

お使いのPython環境にflaskをインストールします。

pip install flask

サーバファイルの用意と起動

そして、app.pyを以下のように作成します。

from flask import Flask, request
import random

app = Flask(__name__)

@app.route('/decision', methods=['POST'])
def buysell_decision() -> int:
    # アカウント番号を取得
    data = request.json
    print(f"\nSender: {data['account']}")
    
    # 取引判断を行う (仮実装:ランダムにシグナルを返す)
    signals = ["Wait", "Buy", "Sell"]
    signal = random.choice(signals)

    # シグナルをログ出力
    print(f"Signal: {signal}\n")
    
    # MetaTraderに応答を返す
    return signal

if __name__ == '__main__':
    app.run(debug=True, host="0.0.0.0")

その後、サーバ起動コマンドを実行し、サーバを起動しておきます。
コマンドは、「python app.py」です。実行すると以下のようにサーバ起動情報が出力されます。

これらの情報の中から、「IPアドレス」と「ポート番号」を控えておきます。

私の場合は、
 ■ IPアドレス   = 192.168.0.7
 ■ ポート番号  =  5000
となります。


MT4/MT5側の実装

こちらの図の左側に位置する、「MT4/MT5」に該当する部分を実装していきます。

ファイルを作成する

まずはファイルを作成します。MetaTraderの上部タブから「File --> New」とクリックし、新しいファイルを作成します。

ExpertAdvisor」を選択し、「Next」をクリックします。

適当なファイル名を付け、「Next」をクリックします。

次の画面ではイベントハンドラーを作成するか聞かれますが、後ほど自分で作るので何も選択せずに「Next」をクリックします。

次の画面では、テスターイベントハンドラーを作成するか聞かれますが、今回は利用しないので、何も選択せずに「Finish」をクリックします。

コードを編集する

すると、サンプルファイルが開かれます。開かれたファイルを以下のように書き換えます。

//+------------------------------------------------------------------+
//|                                                      test-ea.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"


#import "HTTPUtil.dll"
   int SendRequest(string ip_addr, string endpoint, int port, string data, uchar& response[], int responseBufferSize);
#import

int polling_interval = 10; // (s)

uchar response[1024];

double lots = 0.1;
int slippage = 10;
long MagicNumber = 101101;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   // イベントタイマーをセット
   EventSetTimer(polling_interval);
   
   return(INIT_SUCCEEDED);
  }
  
  
//+------------------------------------------------------------------+
//| Timer event handler function                                     |
//+------------------------------------------------------------------+
void OnTimer()
  {
    // Polling関数を定期的に呼び出し
    Polling();
  }


void Polling()
{  
   // どこのサーバにリクエストを送るか指定
   string ip_addr = "192.168.0.7";
   int port = 5000;
   string endpoint = "decision";
   
   // サーバに送信するデータを指定
   long account_number = 0000000111111122;
   string data = StringFormat("{\"account\":\"%d\"}", account_number); 
   
   // リクエストを送信
   int result = SendRequest(ip_addr, endpoint, port, data, response, ArraySize(response));
   
   if ( result >= 0)
   {
      // レスポンスの処理
      Print("Decision: ", CharArrayToString(response));
   }
   else
   {
      // エラー処理
      Print(CharArrayToString(response));
      Print("Error occurred during the request.");
   }
}

//+------------------------------------------------------------------+


//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   string signal = CharArrayToString(response);
   int position_type = -1; // -1 means no-position
   
   // check for existing positions
   for (int i=0; i<PositionsTotal(); i++)
   {
      ulong ticket = PositionGetTicket(i);
      if (PositionSelectByTicket(ticket))
      {
         position_type = PositionGetInteger(POSITION_TYPE);
         break;
      }
   }
   
   /*
   if(GetOrdersTotalByMagic(MagicNumber)==0 && GetPositionsTotal()==0)
   {
      no_position = false;
   }
   */
   
   MqlTradeRequest tradeRequest = {};
   MqlTradeResult tradeResult = {};
   
   if(signal=="Buy")
   {
      if (position_type == POSITION_TYPE_SELL)
      {
         Print("  - Action: CloseAllPositions");
         CloseAllPositions();
      }
      else if (position_type == -1)
      {
         tradeRequest.action = TRADE_ACTION_DEAL;
         tradeRequest.symbol = _Symbol;
         tradeRequest.price = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
         tradeRequest.volume = lots;
         tradeRequest.type = ORDER_TYPE_BUY;
         tradeRequest.magic = MagicNumber;
         
         Print("  - Action: BuyOrder");
         SendOrder(tradeRequest, tradeResult);
      }
      else
      {
         Print("  - Action: None (AlreadyExists)");
      }
   }
   else if(signal=="Sell")
   {
      if (position_type == POSITION_TYPE_BUY)
      {
         Print("  - Action: CloseAllPositions");
         CloseAllPositions();
      }
      else if (position_type == -1)
      {
         tradeRequest.action = TRADE_ACTION_DEAL;
         tradeRequest.symbol = _Symbol;
         tradeRequest.price = SymbolInfoDouble(Symbol(), SYMBOL_BID);
         tradeRequest.volume = lots;
         tradeRequest.type = ORDER_TYPE_SELL;
         tradeRequest.magic = MagicNumber;
           
         Print("  - Action: SellOrder");
         SendOrder(tradeRequest, tradeResult);
      }
      else
      {
         Print("  - Action: None (AlreadyExists)");
      }
   }
   else{
      //Print("Signal is Wait or Already have position");
   }
   
   ArrayInitialize(response, 0);
  }
  
//+------------------------------------------------------------------+


// Funciton to close all positions
void CloseAllPositions()
{
   for (int i=0; i<PositionsTotal(); i++)
   {
      ulong ticket = PositionGetTicket(i);
      if (PositionSelectByTicket(ticket))
      {
         MqlTradeRequest closeRequest = {};
         MqlTradeResult closeResult = {};
         
         closeRequest.action = TRADE_ACTION_DEAL;
         closeRequest.symbol = PositionGetString(POSITION_SYMBOL);
         closeRequest.volume = PositionGetDouble(POSITION_VOLUME);
         closeRequest.position = ticket;
         closeRequest.type = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
         closeRequest.price = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
         closeRequest.magic = MagicNumber;
      
         bool result = OrderSend(closeRequest, closeResult);
      }
   }
}



bool SendOrder(MqlTradeRequest &tradeRequest, MqlTradeResult &tradeResult){
   bool result = OrderSend(tradeRequest, tradeResult);
   
   if(!result)
   {
      Print(tradeResult.retcode, ": ", tradeResult.comment);
   }
   
   return result;
}



/*
double GetPositionsTotal()
{
   Print("PositionTotal: ", PositionsTotal());
   
   if(PositionsTotal()==0)
   {
      return 0;
   }
   else
   {
      for(int i=0; i<PositionsTotal(); i++)
      {
         ulong ticket = PositionGetTicket(i);
         if(PositionSelectByTicket(ticket))
         {
            Print("Ticket: ", ticket, " - Profit: ", PositionGetDouble(POSITION_PROFIT));
         }
      }
      
      return 1;
   }
}


int GetOrdersTotalByMagic(long const magic_number)
 {
  ulong order_ticket;
  int total=0;
  
  Print("OrderTotal: ", PositionsTotal());
  
  // 未決済の注文を全て確認する
  for(int i=0;i<OrdersTotal();i++)
  {
   if (OrderGetInteger(ORDER_MAGIC) == magic_number) total++;
  }
  
  return total;
 }
*/

このコードは以下のような機能を持っています。

  • 指定したサーバと10秒ごとに通信を行う

  • サーバから「Buy, Sell, Wait」などの指示を受け取る

  • 指示に基づいてトレードアクションを実施する

サーバとの通信を行うために、「HTTPUtil」というライブラリを指定しています。こちらは未実装なので以降で実装していきます。

DLLの実装


こちらの図の真ん中、DLLを作成していきます。
このDLLを介してMetaTraderと解析サーバが通信を行う構成としています。

Visual Studioのダウンロードと起動


Visual Studioを起動し、「新しいプロジェクトの作成」を選択します。


検索窓に「DLL」と入力し、「ダイナミックリンクライブラリ(DLL)」を選択します。

「プロジェクト名」「場所 (保存先)」を編集し、「作成」をクリックします。

コードの編集

開かれたファイルを以下のように編集します。(dllmain.cppファイルです。)

// dllmain.cpp : DLL アプリケーションのエントリ ポイントを定義します。
#include "pch.h"

#include <winhttp.h>
#include <stdio.h>
#include <string>
#include <vector>

#ifdef _WIN32
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif

EXPORT int SendRequest(const wchar_t* ip_addr, const wchar_t* endpoint, int port, const wchar_t* data, unsigned char* response, int responseSize) {

	// セッションの作成
	HINTERNET hSession = WinHttpOpen(L"A WinHTTP Program/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);

	if (!hSession) {
		const unsigned char errorMsg[] = "Error in WinHttp Open";
		int msgSize = sizeof(errorMsg);
		memcpy(response, errorMsg, msgSize);

		return -1;
	}


	// サーバに接続
	HINTERNET hConnect = WinHttpConnect(hSession, ip_addr, port, 0);

	if (!hConnect)
	{
		const unsigned char errorMsg[] = "Error in WinHttp Connect";
		int msgSize = sizeof(errorMsg);
		memcpy(response, errorMsg, msgSize);
		WinHttpCloseHandle(hSession);

		return -1;
	}


	// サーバにリクエストを送信
	std::wstring header = L"Content-Type: application/json\r\n";
	std::wstring api_endpoint = L"/";
	api_endpoint += endpoint;

	HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"POST", api_endpoint.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);

	if (!hRequest)
	{
		const unsigned char errorMsg[] = "Error in WinHttp OpenRequest";
		int msgSize = sizeof(errorMsg);
		memcpy(response, errorMsg, msgSize);
		WinHttpCloseHandle(hSession);
		WinHttpCloseHandle(hConnect);

		return -1;
	}

	if (!WinHttpSendRequest(hRequest, header.c_str(), -1, (LPVOID)data, static_cast<DWORD>(wcslen(data) * sizeof(wchar_t)), static_cast<DWORD>(wcslen(data) * sizeof(wchar_t)), 0))
	{
		const unsigned char errorMsg[] = "Error in WinHttp SendRequest";
		int msgSize = sizeof(errorMsg);
		memcpy(response, errorMsg, msgSize);
		WinHttpCloseHandle(hSession);
		WinHttpCloseHandle(hConnect);
		WinHttpCloseHandle(hRequest);

		return -1;
	}

    // レスポンスを受け取る
    DWORD dwSize = 0;
    DWORD dwDownloaded = 0;

    if (!WinHttpReceiveResponse(hRequest, NULL))
    {
        const unsigned char errorMsg[] = "Error in WinHttp ReceiveResponse";
        int msgSize = sizeof(errorMsg);
        memcpy(response, errorMsg, msgSize);
        WinHttpCloseHandle(hSession);
        WinHttpCloseHandle(hConnect);
        WinHttpCloseHandle(hRequest);

        return -1;
    }

    if (!WinHttpQueryDataAvailable(hRequest, &dwSize))
    {
        const unsigned char errorMsg[] = "Error in WinHttp QueryDataAvailable";
        int msgSize = sizeof(errorMsg);
        memcpy(response, errorMsg, msgSize);
        WinHttpCloseHandle(hSession);
        WinHttpCloseHandle(hConnect);
        WinHttpCloseHandle(hRequest);

        return -1;
    }

    std::vector<char> responseData(dwSize + 1);

    if (!WinHttpReadData(hRequest, responseData.data(), dwSize, &dwDownloaded)) {
        const unsigned char errorMsg[] = "Error in WinHttp ReadData";
        int msgSize = sizeof(errorMsg);
        memcpy(response, errorMsg, msgSize);
        WinHttpCloseHandle(hSession);
        WinHttpCloseHandle(hConnect);
        WinHttpCloseHandle(hRequest);

        return -1;
    }
    else {
        memcpy(response, responseData.data(), dwDownloaded);
        response[dwDownloaded] = '\0';
        WinHttpCloseHandle(hSession);
        WinHttpCloseHandle(hConnect);
        WinHttpCloseHandle(hRequest);

        return 0;
    }
}


BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

defファイルの作成

上記の手順で作成したライブラリを外部から呼び出せるように設定していきます。

まず、プロジェクト直下にdefファイルを作成していきます。
画面右側のプロジェクト名を「右クリック」します。

するとメニューが開くので、「追加」 → 「新しい項目」をクリックします。

ファイル名を「HTTPUtil.def」として「追加」をクリックします。

ファイルが開かれるので、以下のコードを追記します。

LIBRARY "HTTPUtil"

EXPORTS
	SendRequest

defファイルの反映

続いて、上記の手順で作成したdefファイルをプロジェクトに反映させます。
上部タブの「プロジェクト」から「プロパティ」を選択します。

「すべての構成」「すべてのプラットフォーム」を選択します。

「リンカー」→ 「入力」 → 「モジュール定義ファイル」とクリックし、
先ほど作成したdefファイルの名前を入力します。(今回はHTTPUtil.def)
「Apply」をクリックし変更を反映します。

Winhttpライブラリを追加する

今回作成するDLLでは、Winhttp.libというライブラリを利用します。
こちらを明示的に追加する必要があるため、先ほどと同じ要領で「依存ファイル」として追加します。

プロジェクトのプロパティを開きます。
「リンカー」→ 「入力」 → 「追加の依存ファイル」をクリックし、
Winhttp.libを追加します。

「Apply」をクリックし変更を反映します。

MT4/MT5から利用可能な状態にする

コードの編集を終えたら、ビルドを行い、MetaTraderで使用可能な状態にします。
上部タブの「ビルド」を選択し、「ソリューションのビルド」を選択します。



作成したものをすべて連携させる

事前にサーバが起動していることを確認しておきましょう。

DLLをMetaTrader側に配置する

Visual Studioでビルドした際に作成された「HTTPUtil.dll」をMetaTrader側にコピーします。
画面右側のプロジェクト名を「右クリック」し、「エクスプローラーでフォルダーを開く」をクリックします。

すると、エクスプローラーが開かれます。
私の環境では「Debug」フォルダ下にdllファイルができるように設定していたため、「x64 / Debug / HTTPUtil.dll」に該当のファイルが存在しています。

「Ctrl + C」を押下し、HTTPUtil.dllをコピーしておきます。

MetaTraderを起動し、コピーしたファイルを配置していきます。
MetaTraderを起動したらMetaEditorに移動します。
画面上部の「File」から「Open Data Folder」をクリックします。

すると、MetaTraderのデータフォルダが開かれます。
「MQL5」 → 「Libralies」とクリックし、Libraliesフォルダ下にコピーしたファイル(HTTPUtil.dll)を配置します。

以上で準備完了です。実際に動かしてみます。
サーバが起動していることを確認し、MetaTraderから作成したEAを実行します。

Navigatorペインに先ほど作成した「test-ea」が存在していますので、
こちらを「ダブルクリック」します。

すると、以下のようにDLLの使用を許可するか尋ねられるので「OK」をクリックします。

画面下部の「Experts」タブをクリックするとEAのログが確認できます。

こちらのログを見ると、「Buy → Sell」を行っているようです。
チャートも確認してみます。

しっかり売買されているようです!

終わりに

今回は、DLLを使用してMetaTraderと外部サーバ(Python)を連携させる方法をご紹介しました。

私は現在、外部サーバでAIを用いたトレードロジックを開発中です。
今後、「学習過程」「バックテスト・フォワードテストの結果」など様々なコンテンツを配信予定です。お楽しみに!!


この記事が気に入ったらサポートをしてみませんか?