【FX/EA】DLLを使用してMT4/MT5とWebサーバで通信する仕組みを実装する
MetaTraderって限界ありませんか?
AIオートトレードのEAを作成する中で、機械学習ライブラリ豊富なPython使いたいなあと思い、、
以下のようなシステムを構築しました。
MetaTrader上で得られるチャート情報を解析し、売買タイミングの指示を出すサーバを構築しています。
このサーバ上には、過去データを使用して学習したAIが搭載されており、チャート情報から、優位性の高い売買タイミングを導き出すことができます。
この時、MetaTraderと解析サーバの通信を仲介するのがDLLとなっています。DLLを使えるとEA開発の幅がグッと広がります。
本記事では、これらの基盤部分を備忘録がてら残しておこうかと思います!
DLLを使用してMT4/MT5とWebサーバで通信する仕組みを実装する
必要なもの
MetaTrader 5 (MetaTrader 5取引プラットフォームの無償ダウンロード)
Visual Studio (Visual Studio 2022 IDE - ソフトウェア開発者向けプログラミング ツール (microsoft.com))
DLLとは
まず、DLLとは何なのかについて軽く解説していきます!
(読み飛ばしていただいても簡易的な実装においては問題ないかと思います。)
下記のような形式で記載していくので、「要約」部分だけ読むのでも十分かと思います。それでは解説していきます。
■ 要約
■ DLLは、複数のアプリケーションで共有して利用することができます
■ 一度DLLを作れば、使い回しができます
■ DLLの利点は3つ。
①コードの再利用
②メモリの節約
③機能追加とメンテナンスが楽
以上が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を用いたトレードロジックを開発中です。
今後、「学習過程」や「バックテスト・フォワードテストの結果」など様々なコンテンツを配信予定です。お楽しみに!!
この記事が気に入ったらサポートをしてみませんか?