
名前つきパイプを使用したMetaTrader 5端末間コミュニケーションにDLLを使用しないソリューション
はじめに
私はときどMetaTrader 5端末間コミュニケーションを可能にする方法について考えました。目標は、端末のひとつで異なるクオート提供者からのティックインディケータと表示ティックを使用することです。
当然の解決法はハードドライブ上の個別ファイルの使用です。一つの端末がファイルにデータを書き込み、他の端末がそれを読み取ります。単独のメッセージを送るのに適したこの手法はクオートの流れにはあまり効果的とは思えません。
わたしはWCFサービスを用いて.NETアプリケーションにクオートをエクスポートする方法について述べたAlexander著のすばらしい記事に出会いました。 そして私がそろそろそれを読み終えるころ、Sergeev著のもうひとつ別の記事が登場したのです。
どちらの記事も私が求めていたものに近いものでしたが、私はDLLを使わない方法を探していました。それはサーバーとクライアントと異なる端末で使われる可能性のあるものです。ウェブ検索をしているとき、名前付きパイプを通信に使うことができることを提案したメモを見つけ、そのパイプを使ったプロセス間通信のためのMSDN仕様 を全部読みました。
名前付きパイプが同一コンピュータ上のまたはイントラネット上の異なるコンピュータ間の通信を助けることを発見し、このアプローチを試すことにしました。
本稿は名前付きパイプの通信を紹介し、CNamedPipesクラスを設計する手順について述べています。また、MetaTrader 5端末間のティックインディケータストリームの検証とシステム間の全体的スループットを扱っています。
1. 名前付きパイプを使用したプロセス間通信
一般的なパイプについて考えるとき、メディアを運搬するのに使われる筒のようなものをイメージします。これもオペレーションシステム上のプロセス間通信手段の一つに関して使われる用語です。 ただ2つのプロセスをつなぐパイプをイメージしてください。この場合、データ交換をするMetaTrader 5端末です。
パイプは匿名でも名前を付けてもかまいません。両者間には大きな違いが2つあります。ひとつめは匿名パイプはネットワーク間では使用できないこと。ふたつめは、2つのプロセスが連携する必要があることです。2つというのは、プロセスのひとつが親プロセス、他方が子プロセスであるものです。名前付きパイプにはこの制約はありません。
パイプを使って通信するためには、サーバー処理は知られている名前でパイプの設定をしなければなりません。パイプ名はストリングで、\\servername\pipe\pipename形式の必要があります。パイプが同一コンピュータ上で使用される場合、サーバー名は省略可能で、その代りドットを使うことができます。:\\.\pipe\pipename.
パイプに接続しようとするクライアントはその名前を知っている必要があります。私は端末を区別するため\\.\pipe\mt[account_number] の命名法をよく使っていますが、命名法は自由に変更可能です。
2. CNamedPipesクラスの実装
名前付パイプの作成と接続のメカニズムは低いレベルの短い記述から始めようと思います。ウィンドウズオペレーティングシステムでは、パイプを操作するすべての関数はkernel32.dllライブラリを介して入手可能です。サーバー側で名前付きパイプのインスタンスを作成する関数はCreateNamedPipe()です。
パイプが作成できたら、サーバーはConnectNamedPipe()関数を呼び、クライアント端末が接続するのを待ちます。問題なく接続すると、ConnectNamedPipe()はゼロ以外の整数を返します。しかし、クライアント端末がCreateNamedPipe()を呼んだ後、 ConnectNamedPipe() を呼ぶ前にうまく接続する場合もあります。この場合は、ConnectNamedPipe()はゼロを返し、 GetLastError()はエラー535 (0X217) : ERROR_PIPE_CONNECTEDを返します。
パイプに書く、またパイプから読むのはファイルアクセスと同じ関数で行われます。
BOOL WINAPI ReadFile( __in HANDLE hFile, __out LPVOID lpBuffer, __in DWORD nNumberOfBytesToRead, __out_opt LPDWORD lpNumberOfBytesRead, __inout_opt LPOVERLAPPED lpOverlapped );
BOOL WINAPI WriteFile( __in HANDLE hFile, __in LPCVOID lpBuffer, __in DWORD nNumberOfBytesToWrite, __out_opt LPDWORD lpNumberOfBytesWritten, __inout_opt LPOVERLAPPED lpOverlapped );
名前付きパイプについて学習して、指導力の低さを隠すためCNamedPipesクラスを設計しました。
今はCNamedPipes.mqhファイルを端末の適切(/include)フォルダに入れ、それをソースコードに含めCNamedPipeオブジェクトを宣言するだけで十分です。
私が設計したクラスは名前付きパイプを操作するいくつかの基本的はメソッドを提示します。
Create(), Connect(), Disconnect(), Open(), Close(), WriteUnicode(), ReadUnicode(), WriteANSI(), ReadANSI(), WriteTick(), ReadTick()
クラスは追加条件によりもっと拡張される可能性があります。
Create() メソッドは与えられた名前でパイプを作成しようとします。端末間の接続を簡素化するために、パイプを使うクライアントのアカウントナンバーとして入力パラメータを 'account'とします。
アカウント名が入力されていなければ、メソッドは現在の端末アカウントナンバーでパイプを開こうとしますパイプがうまく作成できたら、Create()関数はtrueを返します。
//+------------------------------------------------------------------+ /// Create() : try to create a new instance of Named Pipe /// \param account - source terminal account number /// \return true - if created, false otherwise | //+------------------------------------------------------------------+ bool CNamedPipe::Create(int account=0) { if(account==0) pipeNumber=IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)); else pipeNumber=IntegerToString(account); string fullPipeName=pipeNamePrefix+pipeNumber; hPipe=CreateNamedPipeW(fullPipeName, (int)GENERIC_READ|GENERIC_WRITE|(ENUM_PIPE_ACCESS)PIPE_ACCESS_DUPLEX, (ENUM_PIPE_MODE)PIPE_TYPE_RW_BYTE,PIPE_UNLIMITED_INSTANCES, BufferSize*sizeof(ushort),BufferSize*sizeof(ushort),0,NULL); if(hPipe==INVALID_HANDLE_VALUE) return false; else return true; }
Connect() メソッドはクライアントがパイプに接続するのを待ちます。クライアントがうまくパイプに接続できたらtrueを返します。
//+------------------------------------------------------------------+ /// Connect() : wait for a client to connect to a pipe /// \return true - if connected, false otherwise. //+------------------------------------------------------------------+ bool CNamedPipe::Connect(void) { if(ConnectNamedPipe(hPipe,NULL)==false) return(kernel32::GetLastError()==ERROR_PIPE_CONNECTED); else return true; }
Disconnect() メソッドはサーバーをパイプから接続解除します。
//+------------------------------------------------------------------+ /// Disconnect(): disconnect from a pipe /// \return true - if disconnected, false otherwise //+------------------------------------------------------------------+ bool CNamedPipe::Disconnect(void) { return DisconnectNamedPipe(hPipe); }
Open() メソッドはクライアントによって使われ、以前に作成したパイプを開きます。うまく開けたらtrueを返します。なんらかの理由で5秒以内に作成したパイプに接続できない場合、またはパイプを開くのに失敗した場合は、falseを返します。
//+------------------------------------------------------------------+ /// Open() : try to open previously created pipe /// \param account - source terminal account number /// \return true - if successfull, false otherwise. //+------------------------------------------------------------------+ bool CNamedPipe::Open(int account=0) { if(account==0) pipeName=IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)); else pipeName=IntegerToString(account); string fullPipeName=pipeNamePrefix+pipeName; if(hPipe==INVALID_HANDLE_VALUE) { if(WaitNamedPipeW(fullPipeName,5000)==0) { Print("Pipe "+fullPipeName+" not available..."); return false; } hPipe=CreateFileW(fullPipeName,GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL); if(hPipe==INVALID_HANDLE_VALUE) { Print("Pipe open failed"); return false; } } return true; }
Close() メソッドはパイプハンドルを閉じます。
//+------------------------------------------------------------------+ /// Close() : close pipe handle /// \return 0 if successfull, non-zero otherwise //+------------------------------------------------------------------+ int CNamedPipe::Close(void) { return CloseHandle(hPipe); }
次の6つのメソッドはパイプを通して読み書きするのに使用されます。最初の2つのペアのハンドルストリングはUnicodeおよびANSI形式で、とちらも端末間でコマンドやメッセージを送信するのに使用できます。
MQL5のこのストリング変数はUnicode形式のオブジェクトとして保存されます。よって通常はUnicodeメソッドを提供しますが、 MQL5が UnicodeToANSIメソッドを提供するので私は ANSIストリング通信も実装します。 最後の2つのメソッドは名前付きパイプを介してMqlTickオブジェクトの送受信を操作します。
WriteUnicode() メソッドはUnicode形式の文字で構成されるメッセージを書きます。それぞれの文字は2バイトで構成されるので、この関数はパイプに ushort配列を送信します。
//+------------------------------------------------------------------+ /// WriteUnicode() : write Unicode string to a pipe /// \param message - string to send /// \return number of bytes written to a pipe //+------------------------------------------------------------------+ int CNamedPipe::WriteUnicode(string message) { int ushortsToWrite, bytesWritten; ushort UNICODEarray[]; ushortsToWrite = StringToShortArray(message, UNICODEarray); WriteFile(hPipe,ushortsToWrite,sizeof(int),bytesWritten,0); WriteFile(hPipe,UNICODEarray,ushortsToWrite*sizeof(ushort),bytesWritten,0); return bytesWritten; }
ReadUnicode() メソッドはushorts配列を受け取り、ストリングオブジェクトを返します。
//+------------------------------------------------------------------+ /// ReadUnicode(): read unicode string from a pipe /// \return unicode string (MQL5 string) //+------------------------------------------------------------------+ string CNamedPipe::ReadUnicode(void) { string ret; ushort UNICODEarray[STR_SIZE*sizeof(uint)]; int bytesRead, ushortsToRead; ReadFile(hPipe,ushortsToRead,sizeof(int),bytesRead,0); ReadFile(hPipe,UNICODEarray,ushortsToRead*sizeof(ushort),bytesRead,0); if(bytesRead!=0) ret = ShortArrayToString(UNICODEarray); return ret; }
WriteANSI() メソッドはパイプにANSI uchar配列を書きます。
//+------------------------------------------------------------------+ /// WriteANSI() : write ANSI string to a pipe /// \param message - string to send /// \return number of bytes written to a pipe | //+------------------------------------------------------------------+ int CNamedPipe::WriteANSI(string message) { int bytesToWrite, bytesWritten; uchar ANSIarray[]; bytesToWrite = StringToCharArray(message, ANSIarray); WriteFile(hPipe,bytesToWrite,sizeof(int),bytesWritten,0); WriteFile(hPipe,ANSIarray,bytesToWrite,bytesWritten,0); return bytesWritten; }
ReadANSI() メソッドはパイプかuchar配列を読み、ストリングオブジェクトを返します。
//+------------------------------------------------------------------+ /// ReadANSI(): read ANSI string from a pipe /// \return unicode string (MQL5 string) //+------------------------------------------------------------------+ string CNamedPipe::ReadANSI(void) { string ret; uchar ANSIarray[STR_SIZE]; int bytesRead, bytesToRead; ReadFile(hPipe,bytesToRead,sizeof(int),bytesRead,0); ReadFile(hPipe,ANSIarray,bytesToRead,bytesRead,0); if(bytesRead!=0) ret = CharArrayToString(ANSIarray); return ret; }
WriteTick() メソッドはパイプにパイプ単独のMqlTickオブジェクトを書きます。
//+------------------------------------------------------------------+ /// WriteTick() : write MqlTick to a pipe /// \param MqlTick to send /// \return true if tick was written correctly, false otherwise //+------------------------------------------------------------------+ int CNamedPipe::WriteTick(MqlTick &outgoing) { int bytesWritten; WriteFile(hPipe,outgoing,MQLTICK_SIZE,bytesWritten,0); return bytesWritten; }
ReadTick() メソッドはパイプから単独のMqlTickオブジェクトを読みます。パイプが空の場合は、0を返し、そうでない場合はMqlTickオブジェクトのバイト数を返します。
//+------------------------------------------------------------------+ /// ReadTick() : read MqlTick from a pipe /// \return true if tick was read correctly, false otherwise //+------------------------------------------------------------------+ int CNamedPipe::ReadTick(MqlTick &incoming) { int bytesRead; ReadFile(hPipe,incoming,MQLTICK_SIZE,bytesRead,NULL); return bytesRead; } //+------------------------------------------------------------------+
名前付きパイプを操作する基本メソッドはわかったので、2つのMQLプログラムから始めます。クオートを受け取るシンプルなスクリプトとクオート送信のインディケータです。
3. クオート受け取りのサーバースクリプト
例で使うサーバーは名前付きパイプを初期化しクライアントが接続するのを待ちます。クライアントが接続を切ったら、そのクライアントによって合計いくつのティックが受け取られたかを表示し、新しいクライアントが接続するのを待ちます。クライアントが接続を切り、サーバーがグローバル変数 'gvar0' を認めると、サーバーは停止します。'gvar0' 変数が存在しなければ、チャート上で右クリックをしてExpert List オプションを選択し、サーバーを手動で停止することも可能です。
//+------------------------------------------------------------------+ //| NamedPipeServer.mq5 | //| Copyright 2010, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #include <CNamedPipes.mqh> CNamedPipe pipe; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { bool tickReceived; int i=0; if(pipe.Create()==true) while (GlobalVariableCheck("gvar0")==false) { Print("Waiting for client to connect."); if (pipe.Connect()==true) Print("Pipe connected"); while(true) { do { tickReceived=pipe.ReadTick(); if(tickReceived==false) { if(GetError()==ERROR_BROKEN_PIPE) { Print("Client disconnected from pipe "+pipe.Name()); pipe.Disconnect(); break; } } else i++; Print(IntegerToString(i) + "ticks received."); } while(tickReceived==true); if (i>0) { Print(IntegerToString(i) + "ticks received."); i=0; }; if(GlobalVariableCheck("gvar0")==true || (GetError()==ERROR_BROKEN_PIPE)) break; } } pipe.Close(); }
4. クオート送信のシンプルなインディケータ
クオート送信のインディケータはOnInit()メソッド内でパイプを開き、OnCalculate() メソッドが動作するたびに単独の MqlTickを送ります。
//+------------------------------------------------------------------+ //| SendTickPipeIndicator.mq5 | //| Copyright 2010, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #property indicator_chart_window #include <CNamedPipes.mqh> CNamedPipe pipe; int ctx; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { while (!pipe.Open(AccountInfoInteger(ACCOUNT_LOGIN))) { Print("Pipe not created, retrying in 5 seconds..."); if (GlobalVariableCheck("gvar1")==true) break; } ctx = 0; return(0); } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], const double& high[], const double& low[], const double& close[], const long& tick_volume[], const long& volume[], const int& spread[]) { ctx++; MqlTick outgoing; SymbolInfoTick(Symbol(), outgoing); pipe.WriteTick(outgoing); Print(IntegerToString(ctx)+" tick send to server by SendTickPipeClick."); return(rates_total); } //+------------------------------------------------------------------+
5. ひとつのクライアント端末における複数プロバイダからのティックインディケータの実行
個別のティックインディケータの着信クオートを表示したいので事態は複雑化してきました。これができたのはEventChartCustom() メソッドを動作させることでティックインディケータに対し受信ティックをブロードキャストするパイプサーバーを実装したからです。
bid およびask クオートはセミコロンで区切られたひとつのストリングとして送られます。次のようなものです。'1.20223;120225'. 適切なインディケータがOnChartEvent()内のカスタムイベントを操作し、ティックチャートを表示します。
//+------------------------------------------------------------------+ //| NamedPipeServerBroadcaster.mq5 | //| Copyright 2010, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #property script_show_inputs #include <CNamedPipes.mqh> input int account = 0; CNamedPipe pipe; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { bool tickReceived; int i=0; if(pipe.Create(account)==true) while(GlobalVariableCheck("gvar0")==false) { if(pipe.Connect()==true) Print("Pipe connected"); i=0; while(true) { do { tickReceived=pipe.ReadTick(); if(tickReceived==false) { if(kernel32::GetLastError()==ERROR_BROKEN_PIPE) { Print("Client disconnected from pipe "+pipe.GetPipeName()); pipe.Disconnect(); break; } } else { i++; Print(IntegerToString(i)+" ticks received BY server."); string bidask=DoubleToString(pipe.incoming.bid)+";"+DoubleToString(pipe.incoming.ask); long currChart=ChartFirst(); int chart=0; while(chart<100) { EventChartCustom(currChart,6666,0,(double)account,bidask); currChart=ChartNext(currChart); if(currChart==0) break; // Reached the end of the charts list chart++; } if(GlobalVariableCheck("gvar0")==true || (kernel32::GetLastError()==ERROR_BROKEN_PIPE)) break; } } while(tickReceived==true); if(i>0) { Print(IntegerToString(i)+"ticks received."); i=0; }; if(GlobalVariableCheck("gvar0")==true || (kernel32::GetLastError()==ERROR_BROKEN_PIPE)) break; Sleep(100); } } pipe.Close(); }
ティック表示のために、わたしはティックインディケータを MQLmagazineに配置しましたが OnCalculate() メソッドの代わりにOnChartEvent()内部での処理を実装し、条件命令を追加しました。dparamパラメータがパイプ番号に等しく、イベントがCHARTEVENT_CUSTOM+6666に等しいときだけクオートは処理に受け付けられます。
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (dparam==(double)incomingPipe) if(id>CHARTEVENT_CUSTOM) { if(id==CHARTEVENT_CUSTOM+6666) { // Process incoming tick } } else { // Handle the user event } }
下のスクリーンショットでは、3つのティックインディケータがあります。
そのうち2つはパイプを介して受け取られたティックを表示し、パイプを使わない三番目のインディケータは失われたティックがないかチェックするため実行されます。
図1 名前付きパイプを介して受け取られたクオート
添付のスクリーンキャストを見てください。そこには私がインディケータを実行した方法がコメントとしてあります。
図2 インディケータの設定を述べたスクリーンキャスト
6. システムスループット検証
パイプは共有メモリを使うので、通信が速いのです。二台の MetaTrader 5端末間で100 000 および1 000 000ティックを送信するテストをしました。送信スクリプトはWriteTick() 関数を使いGetTickCount()を使ってタイムスパンを測定します。
Print("Sending..."); uint start = GetTickCount(); for (int i=0;i<100000;i++) pipe.WriteTick(outgoing); uint stop = GetTickCount(); Print("Sending took" + IntegerToString(stop-start) + " [ms]"); pipe.Close();
サーバーは受信クオートを読みます。タイムスパンは最初の受信クオートからクライアントが接続を切るまで測定されます。
//+------------------------------------------------------------------+ //| SpeedTestPipeServer.mq5 | //| Copyright 2010, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #property script_show_inputs #include <CNamedPipes.mqh> input int account=0; bool tickReceived; uint start,stop; CNamedPipe pipe; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { int i=0; if(pipe.Create(account)==true) if(pipe.Connect()==true) Print("Pipe connected"); do { tickReceived=pipe.ReadTick(); if(i==0) start=GetTickCount(); if(tickReceived==false) { if(kernel32::GetLastError()==ERROR_BROKEN_PIPE) { Print("Client disconnected from pipe "+pipe.GetPipeName()); pipe.Disconnect(); break; } } else i++; } while(tickReceived==true); stop=GetTickCount(); if(i>0) { Print(IntegerToString(i)+" ticks received."); i=0; }; pipe.Close(); Print("Server: receiving took "+IntegerToString(stop-start)+" [ms]"); } //+------------------------------------------------------------------+
10回サンプル実行をした結果は以下です。
実行 | クオート | 送信時間 [ms] | 受信時間 [ms] |
---|---|---|---|
1 | 100000 | 624 | 624 |
2 | 100000 | 702 | 702 |
3 | 100000 | 687 | 687 |
4 | 100000 | 592 | 608 |
5 | 100000 | 624 | 624 |
6 | 1000000 | 5616 | 5616 |
7 | 1000000 | 5788 | 5788 |
8 | 1000000 | 5928 | 5913 |
9 | 1000000 | 5772 | 5756 |
10 | 1000000 | 5710 | 5710 |
表1 Throughput スピード計測
1 000 000クオートの平均送信時間は170 000 ティック/秒でした。使用したのは、Windows Vista with 2.0GHz T4200 CPUと 3GB RAMです。
おわりに
名前付きパイプを使用したMetaTrader 5端末間コミュニケーション手法を提示しました。端末間でリアルタイムのクオート送信にはこの手法は十分であることが判りました。
CNamedPipesクラスがこれ以上の条件についてより範囲を拡げた内容に使用可能です。たとえば、2つの独立したアカウントにヘッジを作成する場合です。添付のCNamedPipeクラスソースコードをご覧ください。ドキュメンテーションもchmフォーマットで、また本稿執筆のために実装したその他のソースコードも一緒に添付しています。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/115





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索