2011/06/21

MT4用EA開発時代 - その他課題検証/検討結果もろもろ



さて前回は、OrderSelect問題」の調査記事を書き、仮説が誤りであることをコメントを頂いたところまでです(何度もお騒がせしてすみません。。)。今回は、以前の記事で書いたその他諸々の調査結果と検討結果について書いてみたいと思います。

今回調査/検討した項目は以下の通り。

【調査項目】
-------------------------
1.オーダ操作時にTimeCurrent()が最新化されるか
2.GlobalVariableSet()でリトライが必要か?
3.Barsが最大値を超えた場合はどういう振る舞いになるのか
4.「zero divide」発生時の振る舞いについて
-------------------------

【検討項目】
-------------------------
1.start()直後のIsTradeAllowed()は本当に必要か?
2.start()開始時、init()完了確認は不要
3.「OrderSelec問題」のこれから
-------------------------

ではさっそく1つづつ。

【オーダ操作時にTimeCurrent()が最新化されるか】
-----------------------------
これは以前のブログ記事で、OrderModify()時に「ERR_INVALID_TRADE_PARAMETERS」が返却された時にSL/TP決済されていた事を確認する方法について検討し、隙間を無くすために、TimeCurrent()を利用したところの検証です。
これは、オーダ操作でトレードサーバからの応答時にTimeCurrent()が最新化されるという前提だと、穴が無くなるというもの。

でも、ヘルプ上では「ティックデータ受信時にTimeCurrent()が最新化される」という事なので、本当であれば、オーダ操作ではTimeCurrent()は変わらないはず。

●検証方法
 ・オーダ操作(OrderSend/Close)前後でTimeCurrent()を変数に格納。
 ・オーダ操作成功後、RefreshRates()がFALSEであれば、取得していたTimeCurrent()
  を出力
 ・EAを動作させた通貨ペアは、EURUSD1つのみ
 ・気配値は非表示
  ・保有ポジション無しから実行

●検証結果
 オーダ操作前後でTimeCurrent()が異なる結果となった箇所あり。
 (単位は秒なので、同じ値になるケースも多いけど)

●考察
 ・気配値を非表示にして、チャートもEAを動かすチャートだけにすることで、受信する
  ティックデータがEAを動作させた通貨ペアの分だけになるのであれば、オーダ操作
  成功時は、TimeCurrent()が更新されると言えそう。
 ・ただ、トレードサーバまでいかなかった場合(コンテキスト・ビジーとか)だとそうはならないと思う。
 ・検証方法で弱いのは、オーダ操作が成功した時のみという点。トレードサーバ側でエラー検出時
  にどうなるのかは不明なままだけど、期待してもよさそう。

 まぁ、気休め程度の検証ですが。。
-----------------------------

【上記検証プログラム】
------------------------------
#property copyright "Copyright  2011, ahaha_fxtrader"
#property link      "http://blog.livedoor.jp/ahaha_fxtrader/"

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

//---- input parameters
extern int      Magic = 47982738;
extern int       SleepTime=2000;

int init()
{
    MathSrand(TimeCurrent());   
   return(0);
}

int deinit()
{
   return(0);
}
int start()
{
    static int ticket = 0;
    bool found = FALSE;
    datetime st,et;
    
    if( ticket<=0 )
    {
        int type = MathRand()-(32767.0/2.0);
        if( type >=0 )
        {
            st = TimeCurrent();
            ticket = OrderSend(Symbol(),OP_BUY,MarketInfo(Symbol(),MODE_MINLOT),Ask,10,0,0,NULL,Magic);
            if( ticket>0 ) 
            {
                et = TimeCurrent();
                Print("[[ORDER]] Symbol("+Symbol()+") Ticket("+ticket+")");
                if( ! RefreshRates() ) Print("[[TIME]]Send:StartTimeCurrent("+st+") EndTimeCurrent("+et+")");
            }
        }
        else
        {
            st = TimeCurrent();
            ticket = OrderSend(Symbol(),OP_SELL,MarketInfo(Symbol(),MODE_MINLOT),Bid,10,0,0,NULL,Magic);
            if( ticket>0 )
            {
                et = TimeCurrent();
                Print("[[ORDER]] Symbol("+Symbol()+") Ticket("+ticket+")");
                if( ! RefreshRates() ) Print("[[TIME]]Send:StartTimeCurrent("+st+") EndTimeCurrent("+et+")");
            }
        }
    }
    if( ticket<=0 ) return(0);
    
    // オーダ一覧取得ループ
    found = FALSE;
    for( int i=OrdersTotal()-1 ; i>=0 ; i-- )
    {
        // オーダ情報取得
        if( OrderSelect(i,SELECT_BY_POS , MODE_TRADES)==FALSE ) 
        {
            int Errno = GetLastError();
            RefreshRates();
            continue;
        }
         // 対象外であれば次
        if( (OrderMagicNumber() != Magic) || (OrderSymbol() != Symbol()) ) continue;
        
        // 一定時間スリープする
        Sleep(SleepTime);
        
        // チャートのシンボルとOrderSelectのシンボルを出力する
        if( (OrderSymbol()==Symbol()) && (ticket==OrderTicket()) ) Print("[[INFO]] Symbol("+Symbol()+") OrderSymbol("+OrderSymbol()+") Ticket("+ticket+") OrderTicket("+OrderTicket()+")");
        else Print("[[WARN]] Symbol("+Symbol()+") OrderSymbol("+OrderSymbol()+") Ticket("+ticket+") OrderTicket("+OrderTicket()+")");

        st=TimeCurrent();
        if( OrderClose(OrderTicket(),OrderLots(),OrderClosePrice(),100) )
        {
            if( OrderSelect(OrderTicket(),SELECT_BY_TICKET)==TRUE )
            {
                Print("[[CLOSE]] Symbol("+Symbol()+") OrderSymbol("+OrderSymbol()+") Ticket("+ticket+") OrderTicket("+OrderTicket()+") OrderCloseTime("+OrderCloseTime()+")");
                if( !RefreshRates() ) Print("[[TIME]]Close:StartTimeCurrent("+st+") EndTimeCurrent("+TimeCurrent()+")");
            }
            else Print("[[CLOSE]][INFO] OrderSelect(SELECT_BY_TICKET) Failed.Symbol("+Symbol()+") OrderSymbol("+OrderSymbol()+") Ticket("+ticket+") OrderTicket("+OrderTicket()+") OrderCloseTime("+OrderCloseTime()+")");
            ticket = 0;
        }
    }
    
   return(0);
}
------------------------------


【GlobalVariableSet()でリトライが必要か?】
------------------------------
これは、GlobalVariableSet()をするための共通関数で、リトライが必要かどうか。
気にしているのは、ファイル操作になるので、同時に書き込んだときに排他エラーになるんじゃないかという懸念点。

●検証方法
 ・ティックデータ受信毎に、GlobalVariableSet()を同じキーで、10,000,000回ループ。
  1ティックデータ受信時の処理時間は大体3~4秒間ぐらい。
  エラー発生時は、エラーコードと、ErrorDescription()を出力。
 ・動作させた通貨ペアは、4通貨ペア。
 ・start()開始時、終了時にはログ出力して、複数通貨ペアが同時に動作している事を
  確認。
 ・実行時間は30分

●検証結果
 エラー発生なし。

●考察
 誰かが書き込んでる最中は、待たされてるっぽく、排他エラーにはならないっぽい。
 なので、リトライ不用。 #エラー有無の判定はしてるけど。
------------------------------

【Barsが最大値を超えた場合はどういう振る舞いになるのか】
--------------------------------
この検証は、イベントが発生した時のBarsをファイルの大域変数に格納しておいて、MT4を再起動させた際、格納していたBarsを元に制御している。
でも、Barsの値は、MT4の「ツール」→「オプション」メニューの「チャート」タブで設定されている「最大バー数」で制限される事になる。
そこでもし、このBarsが設定された最大バー数を越えた場合にどう振舞うのかを知っておきたい。設定可能な最大値は書籍によると「2,147,483,647」なので、1分足でも4000年ぐらい大丈夫なんだけど、気をつけて設定しておきたいところ。
 
さすがに4000年後は生きてないと思う。。

●検証1
 ・該当設定値を10にして、1分足でチャートを開く。
 ・ティックデータ受信毎にBarsを出力。
  →Barsの値は、11、12と増えていく。今回は20ぐらいになるまでEAを動かした。
●検証2
 ・上記「検証1」の後、設定は同じまま、MT4を再起動して同じEAを実行
 ・Barsの値が、また11、12、と設定した値の次の値に後戻りしてしまう。

ちなみに、上記検証を「ヒストリー内の最大バー数」と「チャートの最大バー数」の両方で検証したが、有効だったのは、「チャートの最大バー数」の方(つまりヘルプの表現通り)。MT4を再起動せいずに、EAを停止して再開した場合は上記動作にならない。

●考察
 Barsが「チャートの最大バー数」を越えてしまうと、今の共通部品の仕組みだとMT4再起動時に
 誤動作する。なので、「チャートの最大バー数」を適正な値に設定が必要。

●処置方法の考察
 書籍を読むと、「チャートの最大バー数」の値を大きくするとメモリを食うので、大きすぎない方
 が良いとかかれているから、このあたりを踏まえて値を決めたほうがよさそう。

 流石に最大値の4000年分は不用なので、すごく多めに見て30年分の1分足とすると、
 単純計算で毎日24Hマーケットが開いているとしても「15,768,000」。丸めてまずは一律
 「15,000,000」に設定しようかと。(現状設定値は「65,000」。5分足でも1年持たない)
 #後はメモリ使用量とのご相談かな。

 その前にMT5への移行が発生すると思いますが。。。
--------------------------------

【「zero divide」発生時の振る舞いについて】
--------------------------------
以前の記事で発生した、「zero divide」(ゼロによる除算)。結局プログラムを見ても、それらしきところが見当たらず、母数になる箇所全てに、母数がゼロであればエラー返却/メール通知する様に修正。
しかし問題は、ゼロによる除算でEAがログ出力だけで停止してしまった点。
つまり、異常に気付けない事が問題点。

なので今回、シンプルにティックデータ受信毎に、意図的にゼロによる除算をしたらどうなるのか確認してみた。

●検証方法
 ・start()開始時にログ出力
 ・ゼロによる除算
 ・start()抜ける前にログ出力

●検証結果
 ゼロによる除算をしても、EAの動作はとまらない。ただし、ゼロによる除算をしたタイミング
 で、start()関数を一旦抜けてしまうみたい。(start()抜ける前のログが出力されなかった)

●考察
 じゃあ、なぜ前回発生時にEAが停止してしまったかと言う話。
 当時のプログラムを見てみると、既にstart()全体を排他制御するロジックが組み込まれていた。
 なので、ロック後にアンロックせずにstart()を抜けてしまった状態。
 なので、少なくとも他の通貨ペアでロックしようとしてもできないから停止状態。

 ただ解せないのは、今の排他ロジックでは、アンロック漏れがあったとしても同じ
 チャート+MAGIC番号であれば、次のティックデータ受信時にロックに成功して、
 処理後にアンロックされるから、処理は継続されるはず。

 アンロック漏れの場合の排他ロジックが間違えてるって事かぁ。。。 
 しかも、排他ロジックのプログラムを見ても、間違えてる箇所が不明。。
 
 しかし少なくとも、MT4が勝手に止まるんじゃなくて、自分でEA動作を止めちゃってる
 って事みたいなので、前回のメール通知処理追加により、エラーに気付かず、他の
 通貨ペアのトレード
が止まらない様になってるので、暫定処置の効果はある事に。 

 あとは再現するのを待つばかり。

 MT4のバグ起因であれば、別の話ですが。。
 #発生したのは、「ODLS.com」 Ver4.00 Build 228(03 Nov 2010)
--------------------------------

調査は以上。

なので、調査項目のステータスを整理すると、以下。

【課題検討結果】
-------------------------
1.オーダ操作時にTimeCurrent()が最新化されるか
  期待通りっぽいので、処置不用。
2.GlobalVariableSet()でリトライが必要か?
  リトライ不用っぽいので、処置不用。
3.Barsが最大値を超えた場合はどういう振る舞いになるのか
  「チャートの最大バー数」を一律「15,000,000」に変更し、変更後はメモリ利用状況を
  注視する。ドキュメントに確認/設定が必要な旨の追記。
4.「zero divide」発生時の振る舞いについて
  MT4上の全てのEAが止まらない様に修正済みなので、最悪の事態は回避されている。
  なので、再現待ち。
-------------------------  

以前の記事で挙げた、ほかの残課題と、前回記事で判った事について。

【その他課題について】
------------------------------
1.start()直後のIsTradeAllowed()は本当に必要か?
  結局、オーダ操作でのリトライ時にIsTradeAllowed()しているから、多重でしている事に。
  いらんもんはいらん。消す。

2.start()開始時、init()完了確認は不要
  前回のブログ記事でコメント頂いたとおり、init()完了後にstart()が動き始めるとのことなので、
  init()完了を待つstart()内ロジックを削除する。
  → 実際にinit()内で1分スリープさせてみて確認もした。

3.「OrderSelec問題」のこれから
  結局振り出しに戻ったけど、一通り他の処置が終ったら、start()関数の排他制御プロパティ
  をオフにする事にして、再現待ちにする。

  ちなみに、周りを混乱させまくっているこの問題、発生した時のソースはコレです。
  ベータリリースした共通部品「ahfw」とAPIが若干異なりますが、構造的にはほぼ同じです。

  → ダウンロード(クリックしたらいきなりzipファイルのダウンロードが開始します)
    ahfw_OrderSelect.zip(18KB)

  ★★ 2011/6/21 22:39 追記 ★★
  → 問題発生時ログファイルダウンロード
    EALOG.zip(61KB)

  被疑箇所を探していただければ幸いです。。。
------------------------------







さて、今回の検証

正しいのは幾つあるだろう。。






そして宿敵を残して、「FXシステムトレード初心者奮闘記」の「MT4用EA開発時代」は、MT4用EA共通部品「ahfw」プログラムを修正して、ドキュメント増強しながら、どこかへと向かうのでした。
MT4用EA共通部品「ahfw」のバグはいつまででも報告お待ちしています。。。くどい?

3 件のコメント:

  1. kartz 【旧ブログから転記】2013/05/07 18:26

    こんにちは。

    ahfw_OrderSelect.zip 内の ahfw.mqh を拝見しました。
    FWClosePositions() の中に怪しい箇所があります。
    以下の★印が私の思考経路です。今回の事件の原因かどうかは分かりませんが…。

    1413行め:
    // ペンディング中オーダの削除
    if( !((OrderType()==OP_BUY) || (OrderType()==OP_SELL)) )
    ★ この中に入っていったとします。

    1421行め:
    if( OrderDelete(OrderTicket() ,White) ) {result = TRUE ; break; }
    ★ OrderDelete() が失敗したとします。
    // ここにきたら削除失敗なので、エラー発生時処理
    Errno = GetLastError();
    ★ FWGetNextActionOnError(Errno) が ACT_OK であるようなエラーだったとします。
    // 注文約定していれば、使う関数が異なるのでリトライループを抜ける
    if( OrderSelect(OrderTicket() , SELECT_BY_TICKET) == TRUE )
    ★ OrderSelect() が失敗したとします。

    1440行め:
    switch( FWGetNextActionOnError(Errno …
    ★ 上述のように、case ACT_OK なので、loop = FALSE; で break; します。

    1457行め:
    if( loop == FALSE ) break;
    ★ ということで、内側の for ループである
    ★ for(int j=0 ; (j<CloseRetryCount) && loop ; j++ ) を抜けます。

    1486行め:
    // 約定済みオーダーのクローズ
    if( (OrderType()==OP_BUY) || (OrderType()==OP_SELL) )
    ★ ここに来ますが、
    ★ 上述のように、OrderSelect() は失敗していますので、OrderXXXX() は不定値です。。。

    幸運を祈ります o(^^)。

    【旧ブログから転記】
    ※ このコメントは、旧ブログで頂いたコメントを、ブログ筆者が転記したものです。

    返信削除
  2. kartz 【旧ブログから転記】2013/05/07 18:27

    補足します。

    FWGetNextActionOnError()
    → case ERR_INVALID_TRADE_PARAMETERS
    → FWGetCurrentOrderCondition()
    → OrderSelect() 失敗続き
    → return(ACT_OK);

    という説明が抜けていました。

    【旧ブログから転記】
    ※ このコメントは、旧ブログで頂いたコメントを、ブログ筆者が転記したものです。

    返信削除
  3. kartz様

    さっそくの調査&応援ありがとうございます!!
    #返信遅くなってすみません。。

    そして、今更気付いたのですが、生ログを一回もブログに載せてなかったですね。。
    なので、ログをダウンロードできる様にしました。
    #ブログ記事にも追記しました。

    http://dl.dropbox.com/u/22484653/%E3%83%96%E3%83%AD%E3%82%B0%E8%A8%98%E4%BA%8B%E7%94%A8/20110621%E6%8A%95%E7%A8%BF%E5%88%86/EALOG.zip


    さっそく本題ですが、指摘された箇所、確かに考慮モレでバグですね。

    > if( OrderSelect(OrderTicket() , SELECT_BY_TICKET) == TRUE )
    > ★ OrderSelect() が失敗したとします。

    この失敗時の考慮が、がっつり抜けてました。。

    > case ERR_INVALID_TRADE_PARAMETERS
    で、ACT_OKが返却されるケースへの考慮もがっつり抜けていました。。。

    なので、ご指摘頂いたパターン以外でも、以下のケースで問題が発生する事に気付けました。

    ・ACT_RETRYで、原因が「ERR_TRADE_TIMEOUT/142/143」で、実はOrderDelete()がトレードサーバ側では成功していた場合だと、リトライ後の上記OrderSelectがFALSEになってしまうので、結果ACT_OKになってしまう。本来成功OrderDelete()に成功したのだから、成功扱いにすべき。
    ただ、ACT_OKの原因が、「ERR_NO_RESULT」の場合は、OrderDelete()ではありえないケースなので、クリティカルエラー扱いにしないといけない。

    -----

    しかし残念ながら、「OrderSelect問題」にはあてはまりませんでした。。。

    ACT_OKの場合には、
    1443行目
    OnError(ERR_CRITICAL , "FWClosePositions() : OrderDelete return ERR_NO_RESULT but unknown case.");
    で、異常メール通知&ログ出力されるはずなのですが、メール通知もログ出力も無かったので、「OrderSelect問題」のケースには当てはまらなかったのです。。

    どちらにしても、指摘頂いた点はそれはそれでバグなので、見つけていただいて良かったです!!

    しかし、他人が作った2KSのソースを短時間で見極めるとは、すごいですね!!

    【旧ブログから転記】
    ※ このコメントは、旧ブログのコメントを、ブログ筆者が転記したものです。

    返信削除