MT4/MQL4とPythonの連携② 名前付きパイプ

自動売買

MT4/MQL4とPythonの連携方法を解説します。前回はソケットを用いた連携方法を解説しました。

MT4/MQL4とPythonの連携➀ ソケット
今回は、MT4/MQL4とPythonの連携方法について解説します。MT5では公式にPythonとの連携機能が用意されているのですが、残念ながら国内業者での採用が多いMT4では用意されていません。

今回は名前付きパイプを用いた連携方法を解説します。

名前付きパイプとは

名前付きパイプとは、プロセス間通信の手法の一つです。Windowsの名前付きパイプはクライアントサーバ型通信に基づいており、1つのサーバーと、1つ以上のクライアント間で、片方向または双方向の通信を行うことができます。

名前付きパイプをオープンした後は、ファイルを読み書きするのと同様にして名前付きパイプに対してデータを読み書きすることができます。全てのプロセスから名前付きパイプがクローズされると、その名前付きパイプは消滅します。名前付きのパイプの詳細はMicrosoftのドキュメントを参照してください。

名前付きパイプを使う

今回の例では、Python側をサーバー、MT4側をクライアントとして名前付きパイプを用います。

最初に、Pythonでサーバーとして名前付きパイプを開くには win32pipe.CreateNamedPipe を使います。win32pipe.PIPE_ACCESS_DUPLEX を指定することで双方向パイプとしています。

import win32pipe
pipename = 'pipe1' # パイプの名前
pipe = win32pipe.CreateNamedPipe(
        '\\\\.\\pipe\\' + pipename, 
        win32pipe.PIPE_ACCESS_DUPLEX,
        win32pipe. PIPE_TYPE_BYTE | win32pipe.PIPE_READMODE_BYTE | win32pipe.PIPE_WAIT,
        1, 256, 256, 0, None)

次に、win32pipe.ConnectNamedPipeでクライアントが接続してくるのを待ちます。

win32pipe.ConnectNamedPipe(pipe, None)

MT4では、FileOpenを用いてパイプに接続します。

const string pipename = "pipe1";  # パイプの名前
int handle;
handle = FileOpen("\\\\.\\pipe\\" + pipename, FILE_READ | FILE_WRITE | FILE_BIN | FILE_ANSI);
if (handle == INVALID_HANDLE) {
   Alert("Can't open pipe");
}

Pythonでは名前付きパイプから文字列の受信を待ちます。win32file.ReadFileを呼び出すと、パイプからデータを読み出せるまで実行がブロックされます。

import win32file

# 読み取りバッファ
buffer = b''

while True:
    try:
        # パイプから1バイト読み取る (パイプが空の場合は、パイプから読めるまで待つ)
        hr, char = win32file.ReadFile(pipe, 1)
    except:
        print("Pipe has been closed")
        break

    buffer += char

    # 改行文字が出現したところで読むのをやめる
    if char == b'\n':
        break

line = buffer.decode('utf-8').replace('\r\n', '')
print(line)

MT4側からパイプに文字列を書き込みます。パイプに書き込みが発生すると、Pythonではwin32file.ReadFileが値を返します。

FileWriteString(handle, line + "\r\n");

パイプが不要になったら、MT4側からパイプをクローズします。

FileClose(handle);

最後に、Python側からもパイプを切断しクローズします。これで、パイプは消滅します。

win32pipe.DisconnectNamedPipe(pipe)
win32api.CloseHandle(pipe)

上記の名前付きパイプ操作手法を用いて実装したサンプルプログラムを以下に掲載しました。

サンプルプログラム (Python)

import win32pipe
import win32file
import win32api
import random
import pandas as pd
from datetime import datetime as dt
pipename = 'MT4PythonPipe1' # パイプの名前

# MT4の定数
OP_BUY = 0 	        # 買いの成行
OP_SELL = 1 	    # 売りの成行
OP_BUYLIMIT = 2 	# 買いの指値
OP_SELLLIMIT = 3 	# 売りの指値
OP_BUYSTOP = 4 	    # 買いの逆指値
OP_SELLSTOP = 5     # 売りの逆指値

# MT4から受信した情報を保存するDataFrame
bardata = pd.DataFrame(columns=['Symbol', 'Open', 'High', 'Low', 'Close'])
orderpool = pd.DataFrame(columns=['OpenTime', 'Type', 'Lots', 'Symbol', 'OpenPrice', 'StopLoss', 'TakeProfit', 'CloseTime', 'ClosePrice', 'Commission', 'Swap', 'Profit', 'Comment', 'MagicNumber', 'Expiration'])

# 名前付きパイプを作成し、インスタンスのハンドルを返す
def createpipe(pipename):
    return win32pipe.CreateNamedPipe(
        '\\\\.\\pipe\\' + pipename, 
        win32pipe.PIPE_ACCESS_DUPLEX,
        win32pipe. PIPE_TYPE_BYTE | win32pipe.PIPE_READMODE_BYTE | win32pipe.PIPE_WAIT,
        1, 256, 256, 0, None)

# MT4がパイプに接続するのを待つ
def connectpipe(pipe):
    return win32pipe.ConnectNamedPipe(pipe, None)

# パイプを切断する
def disconnectpipe(pipe):
    return win32pipe.DisconnectNamedPipe(pipe)

# パイプを削除する
def closepipe(pipe):
    return win32api.CloseHandle(pipe)

# パイプから1行読み、結果をタブで分割して返す
# パイプから1行読めるまで永久に待機する
def readpipe(pipe):
    # 読み取りバッファ
    buffer = b''
    while True:
        try:
            # パイプから1バイト読み取る (パイプが空の場合は、パイプから読めるまで待つ)
            hr, char = win32file.ReadFile(pipe, 1)
        except:
            print(dt.now(), "Pipe has been closed")
            return None

        buffer += char

        if char == b'\n':
            # 改行文字が出現したところで読むのをやめて、値を返す
            print(dt.now(), "Read:", buffer.decode('utf-8').replace('\r\n', ''))
            return buffer.decode('utf-8').replace('\r\n', '').split("\t")

# パイプに文字列を書き込み、書き込めたバイト数を返す
def writepipe(pipe, s):
    s2 = "\t".join(map(str, s)) if type(s) is list else str(s)
    print(dt.now(), "Write:", s2)
    
    try:
        errCode, nBytesWritten = win32file.WriteFile(pipe, (s2 + '\r\n').encode())
    except:
        print(dt.now(), "Pipe has been closed")
        return 0

    return nBytesWritten

pipe = createpipe(pipename) # 名前付きパイプを作成
connectpipe(pipe) # MT4がパイプに接続するのを待つ
print(dt.now(), "connected from MT4")

while True:
    # パイプから1行読む
    received = readpipe(pipe)

    # 読むのに失敗した場合には、終了する
    if received is None:
        break
       
    cmd = None
    if received[0] == 'BarData':
        # MT4から受信した価格情報をDataFrameに保存する
        bardata.loc[dt.strptime(received[2], '%Y.%m.%d %H:%M:%S')] = [
            received[1], float(received[3]), float(received[4]), float(received[5]), float(received[6])
            ]

        # ここでいろいろ売買の判定をする
        # サンプルとしてランダムに命令を出してみる
        r = random.randint(0, 4)
        if r == 0:
            # 何もせず処理を終了
            cmd = "End"
        elif r == 1:
            # Buy
            cmd = ["OrderSend", "EURUSD", OP_BUY, 0.01, "Ask", 20, 0, 0, "Test1"]
        elif r == 2:
            # Sell
            cmd = ["OrderSend", "EURUSD", OP_SELL, 0.01, "Bid", 20, 0, 0, "Test1"]
        else:
            # チケットをランダムに選んで決済する
            if len(orderpool) > 0:
                order = orderpool.sample()
                if order['CloseTime'].values[0] == dt.strptime("1970-01-01", "%Y-%m-%d"):
                    # Close されていない注文なら決済する
                    if order['Type'].values[0] == OP_BUY:
                        price = "Bid"
                    else:
                        price = "Ask"
                    cmd = ["OrderClose", order.index[0], order['Lots'].values[0], price, 20]
                else:
                  cmd = "End"
            else:
                cmd = "End"

    elif received[0] == 'OrderSendResult':
        ticket = int(received[1])
        if ticket == -1:
            print(dt.now(), "Failed to OrderSend")
            cmd = "End"
        else:
            orderpool.loc[ticket] = 0 # 仮にすべて0を入れておく
            cmd = ["TicketInfo", ticket] # チケットの詳細情報を取得する

    elif received[0] == 'OrderCloseResult':
        ticket = int(received[1])
        if ticket == -1:
            print(dt.now(), "Failed to OrderClose")
            cmd = "End"
        else:
            cmd = ["TicketInfo", ticket] # チケットの詳細情報を更新する

    elif received[0] == 'TicketInfoResult':
        # MT4から受信したチケット情報をDataFrameに保存する
        ticket = int(received[1])
        if ticket == -1:
            print(dt.now(), "Failed to TicketInfo")
        else:
            orderpool.loc[ticket] = [
                dt.strptime(received[2], '%Y.%m.%d %H:%M:%S'),
                int(received[3]),
                float(received[4]),
                received[5],
                float(received[6]),
                float(received[7]),
                float(received[8]),
                dt.strptime(received[9], '%Y.%m.%d %H:%M:%S'),
                float(received[10]),
                float(received[11]),
                float(received[12]),
                float(received[13]),
                received[14],
                int(received[15]),
                dt.strptime(received[16], '%Y.%m.%d %H:%M:%S'),
            ]
        cmd = "End"
    else:
        print(dt.now(), "Unkown data received from MT4", received)
        cmd = "End"
    
    if cmd is not None:
        writepipe(pipe, cmd)
    
# パイプをクローズ・削除
disconnectpipe(pipe)
closepipe(pipe)

サンプルプログラム (MT4)

//+------------------------------------------------------------------+
//|                                            connect_to_python.mq4 |
//|                                            Copyright 2021, aifx. |
//|                                                https://aifx.tech |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, aifx."
#property link      "https://aifx.tech"
#property version   "2.00"
#property strict

#include <stderror.mqh> 
#include <stdlib.mqh> 

const string PipeName = "MT4PythonPipe1";

// 名前付き双方向パイプを扱うクラス
class CPipe {
private:
   int handle;
public:
   void CPipe(void);
   void ~CPipe(void);
   bool CPipe::Open(const string pipename);
   void CPipe::Close(void);
   int Read(string& cmd[]);
   uint Write(const string var1, const string var2 = NULL, const string var3 = NULL, const string var4 = NULL,
      const string var5 = NULL, const string var6 = NULL, const string var7 = NULL, const string var8 = NULL,
      const string var9 = NULL, const string var10 = NULL, const string var11 = NULL, const string var12 = NULL,
      const string var13 = NULL, const string var14 = NULL, const string var15 = NULL, const string var16 = NULL,
      const string var17 = NULL, const string var18 = NULL, const string var19 = NULL, const string var20 = NULL,
      const string var21 = NULL, const string var22 = NULL, const string var23 = NULL, const string var24 = NULL,
      const string var25 = NULL, const string var26 = NULL, const string var27 = NULL, const string var28 = NULL,
      const string var29 = NULL, const string var30 = NULL, const string var31 = NULL, const string var32 = NULL);
};

// コンストラクタ
void CPipe::CPipe(void)
{
   handle = INVALID_HANDLE;
}

// デストラクタ
void CPipe::~CPipe(void)
{
   if (handle != INVALID_HANDLE) Close();
}

// 名前付きパイプを開く
bool CPipe::Open(const string pipename)
{
   handle = FileOpen("\\\\.\\pipe\\" + pipename, FILE_READ | FILE_WRITE | FILE_BIN | FILE_ANSI);

   if (handle == INVALID_HANDLE) {
      Alert("Can't open pipe");
      return(false);
   } else {
      return(true);
   }
}

// 名前付きパイプを閉じる
void CPipe::Close(void)
{   
   FileClose(handle);
   handle = INVALID_HANDLE;
}

// パイプからコマンドを読み取る
int CPipe::Read(string& cmd[])
{
   string line;
   while (true)
   {
      string c = FileReadString(handle, 1);
      line += c;
      if (c == "\n") break;
   } 
   line = StringTrimRight(line);
   Print("Received:", line);
   
   return StringSplit(line, '\t', cmd);     
}

// パイプにコマンドを送信する
// MQL4では可変引数を使えない?
uint CPipe::Write(const string var1, const string var2 = NULL, const string var3 = NULL, const string var4 = NULL,
   const string var5 = NULL, const string var6 = NULL, const string var7 = NULL, const string var8 = NULL,
   const string var9 = NULL, const string var10 = NULL, const string var11 = NULL, const string var12 = NULL,
   const string var13 = NULL, const string var14 = NULL, const string var15 = NULL, const string var16 = NULL,
   const string var17 = NULL, const string var18 = NULL, const string var19 = NULL, const string var20 = NULL,
   const string var21 = NULL, const string var22 = NULL, const string var23 = NULL, const string var24 = NULL,
   const string var25 = NULL, const string var26 = NULL, const string var27 = NULL, const string var28 = NULL,
   const string var29 = NULL, const string var30 = NULL, const string var31 = NULL, const string var32 = NULL)
{
   string line = var1;
   if (var2 != NULL) line += "\t" + var2;
   if (var3 != NULL) line += "\t" + var3;
   if (var4 != NULL) line += "\t" + var4;
   if (var5 != NULL) line += "\t" + var5;
   if (var6 != NULL) line += "\t" + var6;
   if (var7 != NULL) line += "\t" + var7;
   if (var8 != NULL) line += "\t" + var8;
   if (var9 != NULL) line += "\t" + var9;
   if (var10 != NULL) line += "\t" + var10;
   if (var11 != NULL) line += "\t" + var11;
   if (var12 != NULL) line += "\t" + var12;
   if (var13 != NULL) line += "\t" + var13;
   if (var14 != NULL) line += "\t" + var14;
   if (var15 != NULL) line += "\t" + var15;
   if (var16 != NULL) line += "\t" + var16;
   if (var17 != NULL) line += "\t" + var17;
   if (var18 != NULL) line += "\t" + var18;
   if (var19 != NULL) line += "\t" + var19;
   if (var20 != NULL) line += "\t" + var20;
   if (var21 != NULL) line += "\t" + var21;
   if (var22 != NULL) line += "\t" + var22;
   if (var23 != NULL) line += "\t" + var23;
   if (var24 != NULL) line += "\t" + var24;
   if (var25 != NULL) line += "\t" + var25;
   if (var26 != NULL) line += "\t" + var26;
   if (var27 != NULL) line += "\t" + var27;
   if (var28 != NULL) line += "\t" + var28;
   if (var29 != NULL) line += "\t" + var29;
   if (var30 != NULL) line += "\t" + var30;
   if (var31 != NULL) line += "\t" + var31;
   if (var32 != NULL) line += "\t" + var32;
   Print("Sent:", line);
   return FileWriteString(handle, line + "\r\n");
}

        
CPipe Pipe;

// Pythonから渡された価格を示す文字列をdoubleに変換する
// "Ask" "Bid" という文字列はそれぞれ現在の Ask, Bid の値に置き換える
inline double convertPrice(const string strPrice)
{
      if (strPrice == "Ask") {
         return Ask;
      } else if (strPrice == "Bid") {
         return Bid;
      } else {
         return StringToDouble(strPrice);
      }
}     

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
   {
   // パイプを開く
   bool ret = Pipe.Open(PipeName);
   if (! ret) {
      Alert("Can't open pipe");
      return(INIT_FAILED);
   }
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   // パイプを閉じる
   Pipe.Close();
  }
  
string PrintError(void)
{
         int check = GetLastError(); 
         if(check != ERR_NO_ERROR) {
            string desc = ErrorDescription(check);
            Alert("OrderSend Fail: ", desc);
            return desc;
         } else {
            return "";
         }
}

//
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
  
   static datetime prev_time = Time[0];

   // 新しい足ができていないときはなにもせずに抜ける
   if(Time[0] == prev_time) {
      return;
   }
   
   prev_time = Time[0];

   // パイプでPythonに最新の足の情報を送る
   Pipe.Write("BarData", Symbol(), TimeToStr(Time[0] , TIME_DATE | TIME_SECONDS),
      DoubleToString(Open[0]), DoubleToString(High[0]),
      DoubleToString(Low[0]), DoubleToString(Close[0]));

   // ENDコマンドが来るまでパイプから1行ずつ文字列を読み込む
   while(true) {
      // パイプから文字列を読み込む
      string received[];
      int n = Pipe.Read(received);
      
      if (n <= 0) {
         Alert("Null command received");
         return;
      }
     
      string sent = "";
      
      if (received[0] == "End") {
         // 処理を終了
         break;
         
      } else if (received[0] == "OrderSend") {
         // 新規注文
         if (n < 8) {
            Alert("OrderSend: Incorrect number of arguments");
            return;
         }
        
         int ticket = OrderSend(
            received[1],   // 通貨ペア名
            StrToInteger(received[2]), // 注文タイプ(0~5)
            StringToDouble(received[3]), // ロット数
            convertPrice(received[4]), // 注文価格
            StrToInteger(received[5]), // スリッページ
            StringToDouble(received[6]), // ストップロス
            StringToDouble(received[7]), // テイクプロフィット
            n > 8 ? received[8] : NULL, // コメント
            n > 9 ? StrToInteger(received[9]) : NULL, // マジックナンバー
            n > 10 ? StringToTime(received[10]) : 0, // 有効期限
            n > 11 ? StringToColor(received[11]) : clrNONE // 色
         );
         string errormsg = ticket == -1 ? PrintError() : "";
         // 結果をPythonに返す (正常終了でチケット番号、異常終了で-1)
         Pipe.Write("OrderSendResult", IntegerToString(ticket), errormsg);
         
      } else if (received[0] == "OrderClose") {
         // 決済注文
         if (n < 5) {
            Alert("OrderClose: Incorrect number of arguments");
            return;
         }
         bool result = OrderClose(
            StrToInteger(received[1]), // Ticket
            StringToDouble(received[2]), // ロット数
            convertPrice(received[3]), // 注文価格
            StrToInteger(received[4]), // スリッページ
            n > 5 ? StringToColor(received[5]) : clrNONE // 色
         );
         string errormsg = result == False ? PrintError() : "";
         // 結果をPythonに返す (正常終了でチケット番号、異常終了で-1)
         Pipe.Write("OrderCloseResult", result ? received[1] : "-1", errormsg);
         
      } else if (received[0] == "OrderCloseBy") {
         // 両建て決済注文
         if (n < 3) {
            Alert("OrderCloseBy: Incorrect number of arguments");
            return;
         }
         bool result = OrderCloseBy(
            StrToInteger(received[1]), // Ticket
            StrToInteger(received[2]), // 別のTicket
            n > 3 ? StringToColor(received[3]) : clrNONE // 色
         );
         string errormsg = result == False ? PrintError() : "";
         // 結果をPythonに返す (正常終了でチケット番号、異常終了で-1)
         Pipe.Write("OrderCloseResult", result ? received[1] : "-1", errormsg);
         
      } else if (received[0] == "OrderModify") {
         // 注文変更
         if (n < 5) {
            Alert("OrderModify: Incorrect number of arguments");
            return;
         }
        
         int result = OrderModify(
            StrToInteger(received[1]), // Ticket
            convertPrice(received[2]), // 注文価格
            StringToDouble(received[3]), // ストップロス
            StringToDouble(received[4]), // テイクプロフィット
            n > 5 ? StringToTime(received[5]) : 0, // 有効期限
            n > 6 ? StringToColor(received[6]) : clrNONE // 色
         );
         string errormsg = result == False ? PrintError() : "";
         // 結果をPythonに返す (正常終了でチケット番号、異常終了で-1)
         Pipe.Write("OrderCloseResult", result ? received[1] : "-1", errormsg);
         
      } else if (received[0] == "OrderDelete") {
         // 保留中の注文を削除
         if (n < 2) {
            Alert("OrderDelete: Incorrect number of arguments");
            return;
         }
         bool result = OrderDelete(
            StrToInteger(received[1]), // Ticket
            n > 2 ? StringToColor(received[2]) : clrNONE // 色
         );
         string errormsg = result == False ? PrintError() : "";
         // 結果をPythonに返す (正常終了でチケット番号、異常終了で-1)
         Pipe.Write("OrderCloseResult", result ? received[1] : "-1", errormsg);
         
      } else if (received[0] == "ListTikets") {
         // オーダーのチケット番号リストを取得してPythonに返す
         // 複数のチケット番号はスペース区切りの文字列で返す
         // 2番目の引数に0を指定すると、エントリーまたは保留中の注文、1を指定すると決済済みの注文を返す
         // 決済済みの注文の表示数はMT4ターミナルの「口座履歴」の表示設定に依存する
         if (n < 2) {
            Alert("ListTicket: Incorrect number of arguments");
            return;
         }
         int type = StrToInteger(received[1]); // 0: エントリーまたは保留中の注文, 1: 決済済みの注文
         int ntickets;
         int pool;
         if (type == 0) {
            ntickets = OrdersTotal();
            pool = MODE_TRADES;
         } else {
            ntickets = OrdersHistoryTotal();
            pool = MODE_HISTORY;
         }
         
         string tickets = "";
         for (int i = 0; i < ntickets; i++) {
            bool result = OrderSelect(i, SELECT_BY_POS, pool);
            if (result) {
               tickets += " " + IntegerToString(OrderTicket());
            } else {
               tickets += " -1";
            }
         }
         Pipe.Write("ListTicketResult", tickets);
         
      } else if (received[0] == "TicketInfo") {
         // チケット情報を取得してPythonに返す
         if (n < 2) {
            Alert("TicketInfo: Incorrect number of arguments");
            return;
         }
         int ticket = StrToInteger(received[1]);
         if (OrderSelect(ticket, SELECT_BY_TICKET) == true) {
            Pipe.Write("TicketInfoResult",
               IntegerToString(OrderTicket()),
               TimeToStr(OrderOpenTime(), TIME_DATE | TIME_SECONDS),
               IntegerToString(OrderType()),
               DoubleToString(OrderLots()),
               OrderSymbol(),
               DoubleToString(OrderOpenPrice()),
               DoubleToString(OrderStopLoss()),
               DoubleToString(OrderTakeProfit()),
               TimeToStr(OrderCloseTime(), TIME_DATE | TIME_SECONDS),
               DoubleToString(OrderClosePrice()),
               DoubleToString(OrderCommission()),
               DoubleToString(OrderSwap()),
               DoubleToString(OrderProfit()),
               OrderComment(),
               IntegerToString(OrderMagicNumber()),
               TimeToStr(OrderExpiration(), TIME_DATE | TIME_SECONDS)
            );
            
         } else {
            Pipe.Write("TicketInfoResult", "-1");
         }
         
      } else {
         Alert("Unknwon command received from Python: " + received[0]);
         return;
      }
    }
  }

ゴールデンウェイジャパン(FXTF MT4)

本サイトの内容は、投資の勧誘を目的としたものではなく、本サイト内の情報に基づいて行った取引の損失について、本サイトは一切の責を負いかねます。当該情報の欠落・誤謬等につきましてもその責を負いかねますのでご了承ください。免責事項もご覧ください。

自動売買

コメント

タイトルとURLをコピーしました