2011/05/29

MT4用EA開発時代 - OrderSelect問題対応(start関数複数同時実行の回避)



さて前回は、エラー警告メールの嵐への対応として幾つかのMT4用EAのバグ対応をしている様子を書きました。今回は、未処置バグだった、1つのMT4で複数EAを同時実行したときに、OrderSelect関連が上手く動作しない事への対処について、検討している様子を書いてみたいと思います。


さて、前回の記事の通り、1つのMT4で複数EA(複数通貨ペアでの同時実行も含む)を動かしていた場合、OrderSelectの結果が格納されている領域が1つだけみたいなので、OrderSelect後のデータ取得が上手く動作しないという「仮説」に対処するためには1つのMT4で複数のstart()関数が同時実行されなければいいと判断。

この実現方法を少し具体的に言うと。

【start関数を同時実行されない様にする方法】
---------------------------
1.start関数直後に、ロックをする。
2.start関数から抜ける時に、アンロックする。
---------------------------

つまり、あるEAがロックして、まだアンロックしていない間は、他のEAでロックしようとすると、ロックしたEAがアンロックするまでは待たされるなる。そしてアンロックされると、待たされていた方のロック処理が終るので、start()関数の続きの処理が開始される。

じゃあ、そもそもそんな事をしていいのか?という話。

【start関数複数同時実行回避していいのかどうか】
--------------------------
●前提条件
 今回の共通部品を使うと、前のバーが完成したあとの最初のティックデータ受信時に、
 発注したり、トレーリングしたり、決済したりする仕様にしているので、複数通貨ペアを
 1つのMT4でEAを実行していた場合、そもそも競合が発生しやすい状況。

この前提を踏まえて、そもそもstart()を同時実行禁止することが、いいのかどうなのかを、
ケース毎に考えて見た。
懸念点として挙がるのが処理遅延がどの程度で、許容可能な範囲なのかどうなのか。

●ケース毎の検討
 1.前のバーが完成した直後の最初のティックデータ受信時
   一斉に、オーダ関連処理が動き始めるが、そもそも同時にオーダ関連の関数を呼び出せる
   のは1つだけ。なので、このケースではstart()が同時処理していたとしても、実質的に処理が
   シリアライズされている状況。
   #同時にオーダすると「ERR_TRADE_CONTEXT_BUSY」が返却されるので。
   そして、オーダ関連関数以外は多分処理時間があまりかからないRefreshRatesは怪しいけど)。
   なので、ロックしてもしなくても、処理遅延にはほとんど影響は無い。
 2.上記「1.」以外の状況で、オーダ関連処理が発生しない場合
   このケースでは、影響を受けるけど、そもそもする事がほとんどないので、
   実質的に問題ない。あるとすれば、SL/TPにひっかかってて、ログ出力処理
   が実行されるぐらい。このログ出力が遅延したところでたいした問題ない。
 3.基本的に上記「2.」だが、ある通貨ペアのリトライが残っている
   他の通貨ペアの処理では、処理時間はおそらく短いので、ほとんど影響を受けない。
   これは、ロック処理でリトライするときのスリープ時間検討時に考えればいい話。

トレード頻度にもよるけど、実質問題なさそう。
懸念点があるとしたら、上記「2.」でもRefreshRates()は動作するし、RefreshRates()は時間が
かかる
というブログ記事もあった点。その記事での調査によると、0~60msec程度だったとのこと。
#確かに解る様な気がする。 
--------------------------

【「オーダ関連関数」って何?】
--------------------------
さて、次はそもそも「オーダ関連関数」って何が含まれるのか?という話。
つまり、「ERR_TRADE_CONTEXT_BUSY」になりうる関数の実行がシリアライズされるので、
それらを精査しておきたい。

それを探るには、「トレード サーバーから返されるエラーコード」を返却する関数を調べればいい。
じゃあ、どの関数が該当するのかをみようとすると、いつもの愛用している有志の日本語ヘルプのこのページを見ればわかる。
#当然英語がわかれば、MT4のヘルプを見れば判るのですが。。

つまり、このヘルプの中で、「トレード サーバーから返されるエラーコード」を返却する関数を精査すればいいという事。("code returned by trade server"と説明に書かれている関数)

精査した結果を列挙してみると。
OrderSend / OrderClose / OrderCloseBy / OrderDelete / OrderModify
の5つだけ。

この判断方法だと、RefreshRates契機では通信が発生する訳ではなさそう。
で、RefreshRates()のヘルプを見ると、

定義済み変数と直列配列内のデータを更新します。この関数は、エキスパート アドバイザーが
 長時間の計算をして、データの更新が必要なときに使用されます。データが更新された場合、TRUE 
 を返します。それ以外の場合は FALSE を返します。データを更新することができない唯一の理由 は、ターミナル クライアントが現在データである場合です。

つまり、1ティックデータ受信後に何回もRefreshRates()をしても、処理中にティックデータを受信していなければ、最初の一回を除いては、何もせずにFALSEが返却されるだけっぽい。
#そもそもstart()開始直前に同等処理をMT4内でしているハズだから、最初の一回目もFALSEの様な気がするけど。
--------------------------

なので、ここまでの結論を纏めると以下。

【対応可否に関しての、調査/検討の結論】
--------------------------
1.OrderSend / OrderClose / OrderCloseBy / OrderDelete / OrderModifyは
  そもそも同時実行不可能
2.上記「1.」のオーダ関連関数以外はきっとあまり時間がかからない。
3.処理時間がかかりそうなのは、RefreshRates()で、長くても60msec程度
  #PC性能次第という話でもありますが。。
  そして、当然Sleep以外でのはなし。
4.上記「1.」~「3.」を踏まえると、やっぱり問題なさそう。
  ただ、ロック時のリトライ処理のスリープ時間は、前述の「ケース毎の検討」の
  「2.」/「3.」や、RefreshRates()の処理時間を踏まえて検討が必要。
--------------------------

じゃあ、次はどうやってロック/アンロックをMQL4でプログラミングするのか、という話。

【ロック/アンロックをする方法】
-------------------------------
簡単に言うと、GlobalVariableSetOnCondition()関数を用いて排他制御する。
これは、いつもお世話になっているブログのこの記事に載っていた方法。

確かに、このGlobalVariableSetOnCondition()という関数仕様をヘルプで見ると、
セマフォ(排他制御用の仕組み)としての用途を想定している旨の記述があるし、OSが
排他制御に使用している、TS(Test&Set)命令というCPU命令と考え方としては同じ。

大域変数はファイルで管理されているので、排他制御ができても確かに不思議ではない。
-------------------------------

【今回共通部品としてどうするのか】
---------------------------------
ロック/アンロックの処理方法で、ブログ記事とのロジックの差異は、以下。

●元ブログ記事のとの差異
 1.二重ロックした場合は成功扱いにした事と、ロックしてないのにアンロックした場合も
   成功扱いにした事。
 2.別のEAがロックしてた時にアンロック共通関数を呼び出した場合は、成功扱いにするものの、
   ロック状態は保持する様にした。
 3.スリープ時間は、最初は100msecで、リトライ回数が増える毎に段階的に長くし、最大2秒   とした。
   これは、「ケース毎の検討」の「2.」/「3.」の状況ではスリープ時間を短くした方が遅延が少なく
   なると思ったから。
   100secのゆえんは、前述のRefreshRates()が最大60msecという事を踏まえて、多めに丸めた。
   2秒のゆえんは、IsStopped()の2.5秒ルールと、処理時間のおおまかな見積もりが
   500msecという事を踏まえて、「2.5秒-0.5秒=2秒」という理屈。

ちなみに、なぜ上記「1.」/「2.」の様にしたかと言うと。
二重ロックというプログラムミスでも成功できる様に。
アンロック漏れに対処するためdeinit()でもアンロック処理を入れたかったから。こうして
 おけば、もしプログラムミスによるアンロック漏れが発生して、トレードが停止してしまった
 場合でも、MT4を再起動させれば、自動でアンロックされる事になるから。
 つまり、暫定処置をすばやくできる点。
・それでもアンロック漏れに対処できないのは、MT4が突如プロセスダウンしてしまった場合。
 この場合は、MT4起動後に一旦該当大域変数にゼロを設定する事で対応する。
-------------------------------

じゃあ、次は具体的なMQL4のプログラムは?という話。
#このブログで初のMQL4のプログラムかも。。結構恥ずかしい。。

【ロック/アンロックのMQL4プログラム】
-----------------------------
1.プログラムの冒頭に、以下を記述。
#define KEY_SEMSTART "SEM_START"
double SemValue = 0.0;
int Errno = 0;
2.init関数内で、SemValueをWindowHandle()とMAGICナンバーからそのEAで一意に
  なる様な値に設定している。
SemValue = [MAGICナンバー]/1000000.0+WindowHandle(Symbol(),Period())*100.0;
3.その他
  ・ロジック中のOnError()関数は独自の共通関数で、ログ出力したり、
   エラーレベルによってメール送信する関数。
  ・ロジック中のFWRefreshRates()関数は独自の共通関数で、RefreshRates()が
   主な処理内容。
4.ロック関数
  本当は、長時間ロックできなかったら、警告メールを出したほうがいいと思うけど、
  まだそれは書いていない。あと、「ERR_GLOBAL_VARIABLE_NOT_FOUND」を成功扱い
  にしているのは、取得した値がゼロだったら、「ERR_GLOBAL_VARIABLE_NOT_FOUND」
  が返ってきてたので。
bool FWLockStart()
{
    int i=0;
    // テスターだと発生しないので、正常終了する
    if( IsTesting() ) return(TRUE);

    // 成功するまで繰り返す
    while( !IsStopped() )
    {
        if( GlobalVariableSetOnCondition(KEY_SEMSTART,SemValue,0) ) return(TRUE);
        if( GlobalVariableSetOnCondition(KEY_SEMSTART,SemValue,SemValue) ) return(TRUE);
        Errno = GetLastError();
        switch(Errno)
        {
            case ERR_STRING_PARAMETER_EXPECTED:
                OnError(ERR_CRITICAL , "FWLockStart:GlobalVariableSetOnCondition failed");
                return(FALSE);
            default:
                break;
        }
        i = i+1;
        if( i<20 ) Sleep(100);
        else if( i<40 ) Sleep(250);
        else if( i<80 ) Sleep(500);
        else Sleep(2000);
        FWRefreshRates();
    }
    return(FALSE);
}
あと、後半のSleppしている箇所の各数字に関しては、「100」と「2000」以外にこれといって
  意味は無い。雰囲気として、徐々にリトライ間隔を伸ばしていきたかったという程度。

5.アンロック関数
  ここでのミソは、大域変数自体がファイルI/Oしているので、GlobalVariableSet
  の時、ファイル自体の競合が発生して、「ERR_GLOBAL_VARIABLES_PROCESSING」が返却
  される可能性があるという事。なので、アンロックでもリトライすることに。
  でもできるだけリトライしたいので、IsStopped()がTRUEの時は、最大2秒間まで
  ループを続ける事にした。
bool FWUnlockStart()
{
    // アンロックでリトライが発生したら、IsStoppedでも最大2秒まで繰り返す
    int elap = GetTickCount()+2*1000;

    // テスターだと発生しないので、正常終了する
    if( IsTesting() ) return(TRUE);

    // 成功するまで繰り返す
    while( TRUE )
    {
        // 現在のセマフォ値を取得する
        double wkSemVal = GlobalVariableGet(KEY_SEMSTART);
        Errno = GetLastError();
        switch( Errno )
        {
            case ERR_STRING_PARAMETER_EXPECTED:
                OnError(ERR_CRITICAL , "FWUnlockStart:GlobalValiableGet error.");
                return(FALSE);
        }

        // 他でロックしているか、ロックがされていなければ、正常リターン
        if( wkSemVal != SemValue ) return(TRUE);
        
        // アンロックする
        if( GlobalVariableSet(KEY_SEMSTART,0) != 0 ) return(TRUE);
        // アンロックに失敗した場合
        Errno = GetLastError();
        switch(Errno)
        {
            case ERR_STRING_PARAMETER_EXPECTED:
                OnError(ERR_CRITICAL , "FWUnlockStart:GlobalVariableSet error.");
                return(FALSE);
            case ERR_GLOBAL_VARIABLE_NOT_FOUND:
                return(TRUE);
            default:
                break;
        }
        if( IsStopped() && (elap<GetTickCount()) ) break;
        Sleep(250);
    }
    return(TRUE);
}
-----------------------------





色々もっともらしく書いたが、

実は、まだ動作実績が少ない。

だれか、考慮漏れ指摘してください。




そして、なんだかカサ増し感のある記事でお茶を濁して、「FXシステムトレード初心者奮闘記」の「MT4用EA開発時代」は、続きをどうしよう。。。
#バグ対応してから、今のところ警告メール受け取っていない!!1日程度だけど。。

0 件のコメント:

コメントを投稿