OrderSelect関数を使いこなす

 OrderSelect関数は様々な場面で頻繁に使用される関数であると共に、MQL4プログラミングの中でも一番奥が深い関数でもあります。頻繁に使用される関数であるからこそ、OrderSelect関数を正しく理解しなくてはいけません。適切でないOrderSelect処理はバグの原因となり、ロジック通りの取引ができなくなってしまいます。

OrderSelect関数のおさらい

 まずはOrderSelect関数についてのおさらいです。
公式のドキュメント

bool  OrderSelect(
   int     index,            // index or order ticket
   int     select,           // flag
   int     pool=MODE_TRADES  // mode
   );

 OrderSelect関数では3つの引数を指定して、該当のオーダーを選択します。オーダーの選択に成功した場合はtrue、失敗した場合はfalseが返り値となります。OrderSelect関数は大きく分けて3通りの使い方があります。

  1. インデックス番号を指定して、トレーディングプールから選択
  2. インデックス番号を指定して、ヒストリープールから選択
  3. チケット番号を使用して選択
     1と2のインデックス番号を使用してオーダーを選択する場合には、第二引数にはSELECT_BY_POSを指定します。3のチケット番号を使用してオーダーを選択する場合には、SELECT_BY_TICKETを指定します。
     トレーディングプールからオーダーを選択する場合には、第三引数にMODE_TRADESを指定します。第三引数はデフォルトがMODE_TRADESなので、トレーディングプールから選択する場合には省略することができます。ヒストリープールから選択する場合には、第三引数にMODE_HISTORYを指定します。
     第二引数にSELECT_BY_POSを指定した場合、第一引数にはオーダープールのインデックス番号を指定します。第二引数にSELECT_BY_TICKETを指定した場合、第一引数にはチケット番号を指定します。第二引数にSELECT_BY_TICKETを指定すると、第三引数は無視されます。チケット番号でOrderSelectすると、どちらか存在する方から選択されます。どちらのプールから選択されたか知りたい場合は、OrderCloseTime関数などを使って決済済みかを判断します。

 幾つか専門用語が出てきたため解説いたします。

トレーディングプール

 トレーディングプール(trading pool)とは未決済の保有中ポジションと未約定の待機注文が集まって管理されている場所のことです。ターミナルの取引タブのような場所だと考えると、イメージしやすいかと思います。 MT4ターミナルの取引タブ  上の図では3つの保有中ポジションと、1つの待機注文があります。この4つのオーダーはいずれも、トレーディングプールによって管理されています。OrderSelect関数を使ってこれらのオーダーを選択するには、第三引数にMODE_TRADESを指定して、トレーディングプールを使用しましょう。

ヒストリープール

 ヒストリープール(history pool)とは、決済済みのオーダーとキャンセルされた待機注文が集まって管理されている場所のことです。こちらはターミナルの口座履歴のような場所と考えて下さい。 MT4ターミナルの口座履歴タブ
 決済済みオーダーと、キャンセルされたオーダー以外にも、入出金やOANDA Japanのスワップなどのようにブローカーによって挿入された履歴もヒストリープールによって管理されます。これらのオーダーを選択するには、第二引数にSELECT_BY_POSを、第三引数にMODE_HISTORYを指定しましょう。 トレーディングプールとオーダープールの解説図

オーダープール

 オーダープール(order pool)とは、トレーディングプールとヒストリープールをまとめた呼称です。

インデックス

 インデックス(index)とは、オーダープールに集まっているオーダーに割り振られた管理番号のことです。このインデックスの番号は、オーダープールに来た順番に、0, 1, 2, ... と割り振られていきます。この番号は必ず0から始まる連番となります。
 OrderSelect関数を使う上では、これらの用語が重要となります。中でもインデックスの動きが特に重要となります。

オーダープールの基礎

 まずはオーダープールの基本的な動きについて理解しましょう。  新しいオープンポジション、もしくは待機注文が作られたタイミングで、トレーディングプールにオーダーが追加されます。 トレーディングプールへのオーダー追加のイメージ

トレーディングプールへのオーダー追加のイメージ

 決済または待機注文のキャンセルがされると、トレーディングプールのオーダーが減り、ヒストリープールに新たなインデックスのオーダーが追加されます。これ以外に、入出金をおこなった場合などにも、ヒストリープールにオーダーが追加されます。  オーダーを決済すると、トレーディングプールのインデックスに欠番が発生することがあります。その場合にインデックスは、0からの連番となるように再計算がおこなわれます。 決済時のインデックスの変化

決済時のインデックスの変化

 上の図ではトレーディングプール内のindex 2のオーダーが決済されたことで、index 2の情報はヒストリープール内にindex 4として新たに追加されています。index 2が決済され欠番となったため、トレーディングプール内ではインデックスの再計算が起こります。インデックスはオーダープールに来た順に0から割り振られます。この場合ですと、index 0と1はそのままで、index 3が2へ、index 4が3となることで、再び0からの連番となります。  これがオーダープールとインデックスの基本的な流れになっています。続いて、オーダープールにオーダーが追加されるタイミングについて、もう少し詳しく見ていきましょう。

トレーディンプールにオーダーが追加されるタイミング

 トレーディングプールにオーダーが追加されるタイミングは4つあります。

  1. 新規エントリー時(成り行き買い・成り行き売り)
  2. 待機注文時(指値買い・指値売り・逆指値買い・逆指値売り)
  3. 部分決済時
  4. 待機注文からエントリー時

 1と2に関しては特に説明する必要は無いかと思います。新しい取引がされたため、トレーディングプールにもオーダーが追加されます。
 部分決済は少々特殊な処理となっております。部分決済が行われた場合、一度そのオーダーは決済され、新しいチケット番号で残りのロット数のオーダーが作られます。ターミナルの取引タブではオーダーの総数に変化は無いように見えますが、トレーディングプールではオーダーが削除されてから、オーダーの追加が行われています。インデックスはオーダープールに追加された順に割り振られるため、部分決済の際にインデックス番号が変わることがあります。例えば4ポジションを保有していた際に、index 1のオーダーを部分決済したとします。この時index 1は一旦トレーディングプールから削除されるため、index 2が1へ繰り上がり、index 3が2へ繰り上がります。そして部分決済後の旧index 1のオーダーが、index 3として新たに追加されます。部分決済をおこなった場合は、ロット数だけでなくチケット番号とインデックスも変わるということを覚えておいてください。
 待機注文が約定し、エントリーされた場合にもトレーディングプールにオーダーの追加がおこなわれます。これも先ほどの部分決済の場合と同様で、一旦待機注文だったオーダーが削除されます。その後、新しいチケット番号が割り振られ、トレーディングプールにオーダーが追加されています。下の2つの画像は、待機注文のエントリー前とエントリー後のチケット番号です。エントリー前は146890316でしたが、エントリー後は146890590に変わっています。 待機注文のチケット番号

待機注文のチケット番号

待機注文がエントリー後のチケット番号

待機注文がエントリー後のチケット番号

 余談ですが、部分決済時と待機注文のエントリー時にチケット番号が変わるというのは、非常に重要な要素です。OrderSend関数の返り値として受け取ったチケット番号を利用して決済するEAの場合、部分決済後にチケット番号が変わるため、無効なチケット番号となってしまいます。部分決済後のオーダーを決済するには、チケット番号を再取得しなくてはいけません。待機注文でエントリーするEAにおいても、待機状態のチケット番号とエントリー後のチケット番号が異なりますので、エントリー後にオーダーを決済するには、チケット番号を再取得しなくてはいけません。EA側で部分決済することが無くとも、EAが保有したポジションを利用者が裁量で部分決済してしまうこともあります。「EAのポジションを手動で切るな!」と思うかもしれませんが、本当に様々な使い方する人がいます。このようなケース時に不具合を起こさないためにも、チケット番号は外部変数に保持して使いまわすのではなく、必要なタイミング(決済前、注文変更前など)でOrderSelect関数を使い、再取得して利用するのが良いでしょう。

トレーディンプールからオーダーが削除されるタイミング

 トレーディングプールからオーダーが削除されるタイミングも合計4つあります。

  1. オープンポジション決済時
  2. 待機注文キャンセル時
  3. 部分決済時
  4. 待機注文のエントリー時

 1と2は説明せずともイメージできるかと思います。決済・キャンセルされたことでトレーディングプールからオーダーが消えます。補足するのであれば、待機注文がキャンセルされる方法には、手動でキャンセル、EAによるOrderDelete関数からのキャンセル、設定した注文の有効期限切れによるキャンセルの3種類があります。3と4については、トレーディングプールへの追加の際に説明した通りです。一旦オーダーが削除され、新たにオーダーが追加され直すという流れで処理が行われいます。

ヒストリープールにオーダーが追加されるタイミング

 ヒストリープールにオーダーが追加されるタイミングは合計7通りに分類できます。

  1. オープンポジションが決済された時
  2. 待機注文がキャンセルされた時
  3. 部分決済がされた時
  4. 待機注文がエントリーした時
  5. 入金・出金を口座に反映された時
  6. ブローカーによる口座残高の調整がされた時
  7. 口座履歴の期間が変更された時

 1と2については特に説明は不要かと思われます。部分決済された場合は、決済された分の取引数量の決済オーダーが作られ、ヒストリープールに追加されます。トレーディングプールに残ったオーダーについても、決済される毎にヒストリープールに追加されます。そのため部分決済を使用している場合、1回のエントリーであっても、ヒストリープールには複数のオーダーが作られることとなります。4について、待機注文がエントリーすると、それまでの待機注文のオーダーはヒストリープールに追加されます。トレーディングプールには新たにオープンポジションのとしてのオーダーが追加されます。このポジションが決済されると、その情報もヒストリープールに追加されるため、待機注文からエントリーすると、取引としては1回ですが、ヒストリープールには2つの取引情報が作られることとなります。 待機注文のチケット番号

待機注文のチケット番号

待機注文がエントリー後のチケット番号

待機注文がエントリー後のチケット番号
 上の2つの画像はトレーディングプールにオーダーが追加されるタイミングの説明でも使用した画像です。待機注文のエントリー前後でチケット番号が変わっています。下の画像はこのポジションが決済された場合の口座履歴の画像です。上の2つの画像と、下の画像のチケット番号を見比べてみて下さい。チケット番号が一致していますね。このように待機注文からエントリーして、決済された場合には、ヒストリープールにはオーダーが2つ作られます。 待機注文決済後の2つのオーダー
待機注文決済後の2つのオーダー

 入金額や出金額に関する情報もヒストリープールに追加されるため、入金や出金の手続きをおこなうことでヒストリープールにオーダーが追加されます。ブローカーによって口座残高の調整がおこなわれ、オーダーが挿入されることがあります。これはOANDA Japanで取引をおこなったことのある方なら経験があるのではないでしょうか?OANDA Japanの口座でポジションを保有しても、オープンポジションにはスワップポイントが付きません。代わりに日毎にスワップポイント分だけ口座残高の増減がおこなわれ、口座残高が調整がされます。ちなみにOANDA Japanのベーシック口座やスタンダード口座では、スワップポイントの計算が秒単位でおこなわれるため、日を跨がずにポジションを決済してもスワップポイント分の調整がおこなわれることがあります。 口座残高調整の例

口座残高調整の例

ヒストリープールからオーダーが削除されるタイミング

 ヒストリープールに存在するオーダーの数は、口座履歴の件数と同じです。そのため、口座履歴の期間を変更した際にヒストリープールのオーダーの数が増減します。下の図では、口座履歴の期間を全期間に設定しています。 口座履歴の例1

口座履歴の例1

 口座履歴に全期間の情報を表示した状態で、下記のコードを実行してヒストリープールのオーダーの件数を取得してみます。

int OnInit()
{  
    Print("OrdersHistoryTotal = ", OrdersHistoryTotal());

    return(INIT_SUCCEEDED);
}

 このコードを実行した結果、私の環境では76件のオーダーがありました。 OrdersHistoryTotalの実行結果

OrdersHistoryTotalの実行結果

 続いて、意図的に口座履歴の期間を絞り、件数を減らしてみました。下の図はその結果です。 口座履歴の例2

口座履歴の例2

 かなり期間を絞ったので、口座履歴には1件もオーダーが表示されていません。この状態で、先ほどのコードを実行してみます。 OrderHistoryTotalの実行結果2

OrderHistoryTotalの実行結果2

 この場合の実行結果は0件となりました。このように、口座履歴に表示する期間を変えることで、ヒストリープールの件数は変化します。口座履歴の期間が変わるのは、手動で変更したタイミングだけではありません。MT4の再起動時に、知らぬ間に変わってしまう場合もあります。口座履歴に全期間を指定しても、口座開設時からの全履歴が取得できるわけではありません。一部の会社では、一定期間以上経過した古い取引履歴については配信されなります。どの程度の期間で配信されなくなるかは、会社によって変わります。このように、口座履歴の件数が変化するタイミングで、ヒストリープールの件数も増減します。

堅牢なOrderSelect処理を考える

 さて、OrderSelect関数とオーダープールの仕組みが分かったかと思います。不安な人は次へ進む前に、「OrderSelect関数のおさらい」から読み直して下さい。これからは実際の運用を想定したOrderSelect関数の使い方について考えていきます。

オープンポジションの合計損益を取得する関数

 まずはトレーディングプールから情報を取得する方法を考えます。ここでは、オープンポジションの合計損益を取得する関数を例に考えていきます。関数を作る上でいくつか条件があります。

  • 関数名は、getTotalProfitとする
  • この関数はTestEAという名称のEAに実装する
  • このEAは、複数のポジションを保有することがある
  • 運用口座では、裁量取引や他のEAが運用されることがある
  • 各EAには別々のマジックナンバー(0以外)が使用されている
  • スワップ・手数料・税は損益に含めない

 これらの前提条件を踏まえて、正確に損益を取得できる関数を作成しましょう。取得した合計損益は、追加エントリーの条件や決済条件に使用できます。もし間違った損益を取得してしまうと、おかしな位置でエントリーや決済が発生してしまいます。手元に開発環境のある方は、メタエディターを開いて自分で関数を作ってみて下さい。そして作った関数と解説を見比べてみてください。

 下のコードは私が、「普通のEA開発者ならこう書くかな?」と考えて書いたコードです。

getTotalProfit関数のサンプル1

double getTotalProfit(int magic)
{
    double total = 0;

    for(int i = 0; i < OrdersTotal(); i++) {
        if(OrderSelect(i, SELECT_BY_POS)) {
            if(OrderMagicNumber() != magic) continue;
            if(OrderSymbol() != Symbol()) continue;

            total += OrderProfit();
        }
    }

    return(total);
}

 どうでしょうか?ほとんどの方はこのような処理を書いたのではないでしょうか?

 今回はオープンポジションから取得するので、OrderSelect関数の第二引数はSELECT_BY_POSを指定して、第三引数は省略します。選択されたオーダーのマジックナンバーと、引数で受け取ったmagicの値が一致しているかチェックします。マジックナンバーが一致していれば通貨ペアをチェックします。通貨ペアも一致していれば、変数totalに損益を加算します。そしてこの処理をfor文を使ってOrdersTotalの数だけ繰り返した後、最終的なtotalの値を返り値として返却します。サンプル1の関数で違いが出るとすれば、for文のiを昇順で実行するか、降順で実行するかくらいではないでしょうか?
 さて、サンプル1の処理ですが、この処理では合格ではありません。サンプル1の関数はバックテストでは常に正確な損益を返却しますが、実際の運用時には間違った損益を返却する可能性が潜んでいます。例を交えながら解説していきます。以下のような5つのポジションを保有中だったとしましょう。 保有中のポジション情報

保有中のポジション情報

 index 0~4まで5つのポジションが存在しています。マジックナンバーは0、1、2の3種類があります。仮にTestEAのマジックナンバーは1としましょう。その場合TestEAのポジションは、index 0, 2, 4の3つです。TestEAの正しい合計損益は+6,000ですね。
 この先の説明をわかりやすくするために、TestEAの仕様を追加させてください。TestEAは①合計損益が+10,000以上の利益で利食い、②-5,000以下の損失で損切りをおこなうとします。現在は合計損益が+6,000ですので、利食いも損切りもせずに現状維持が正しい処理です。この仕様も踏まえた上で、サンプル1の問題点を考えていきましょう。

処理中にポジションが増減した場合を考える

 実際の運用ではgetTotalProfit関数の実行中に、ポジションが増減する可能性があります。TestEAと同じ口座で、裁量でも取引をする場合や、複数のEAを運用している場合に発生します。ポジションが増加する分には問題はないでしょう。オーダーはトレーディングプールに入った順に並ぶため、新しく追加されたオーダーはindexが5として割り振られます。前提条件を考えれば、追加されたオーダーのマジックナンバーは1ではないため、OrderSelectしてチェックする必要はありません。実際にはfor文の条件式にOrdersTotalと書いているため、処理中に増えたオーダーも処理の対象となります。ただしfor文でiを降順で書いている場合や、ポジションの合計をfor文の前に計算している場合には新規のオーダーが計算されません。EA以外のオーダー情報も使用する取引ツールなどでは注意しましょう。

//iを降順に処理する例
for(int i = OrdersTotal() - 1; i >= 0; i--) {

}

//for文の外でオーダー数を計算している例
int orders_total = OrdersTotal();
for(int i = 0; i < orders_total; i++) {

}

 どうやら関数の処理中にオーダーが増加しても問題は無さそうです。しかしオーダーが減る場合は問題が生じます。index 0の損益取得が終わり、index 1のOrderSelectが完了した時点で、index 1のオーダーが決済されたとします。この時インデックスは再計算されます。index 1の次はindex2のオーダーが選択されますが、インデックスが変わったためindex 2のオーダーは元々index 3だったオーダーです。そして次に選択されるindex 3のオーダーは元々index 4だったオーダーです。index 3の処理が終わったところで、iがOrdersTotalの値を超えるため、ループ処理は終了です。元々index 2のオーダーはというと、一度も処理がされないまま関数が終わってしまうのです。
 関数が返却する値はどうなるかというと、+12,000となります。元index 2の-6,000が計算されてないため、実際の損益よりも6,000多くなっています。合計損益が10,000を超えているとの結果であるため、EAは利食いをおこなってしまうでしょう。ロジックに無い状況での決済、バグの発生です。 インデックスの変化

インデックスの変化

 先ほどの例はiを昇順で処理した場合でした。iを降順で処理をした場合はどうでしょうか?index 4, index 3と処理し、index 2のオーダーを選択した時点で、index 1が決済されたとしましょう。次のindex 1の選択では、元index 2のオーダーが再び選択されてしまいます。index 0は変わらないので一緒ですね。これによって最終的な結果は以下のようになります。

8,000 - 6,000 - 6,000 + 4,000 = 0

 -6,000が2回計算されてしまい、今度は本来よりも6,000少ない損益となってしまいました。降順に処理した場合の例でも、正しい値が計算できないケースがあるようです。ロジック次第では取引に影響を与えてしまいます。
 ではどのような処理であれば、処理中のオーダーの増減に対応できるでしょうか?対策を考えていきましょう。この関数を実行すると、2つのケースが想定できます。

  • 処理が問題なく終了し、正しい結果を取得できた場合
  • オーダーの増減が発生し、正しい結果を取得できなかった場合
     そこで関数の戻り値をbool型に変更して、成功した場合はtrue, 失敗した場合はfalseを受け取るように変更しましょう。合計損益は返り値として受け取るのではなく、変数を参照渡しとすることで取得します。関数の結果がtrueの場合は合計損益を使用して処理を続行します。falseの場合は処理を進めずに、次のティックでリトライした方が安全です。オーダーの増減があったかは、for文を実行する前のOrdersTotalの値と、実行後のOrdersTotalの結果を比較します。処理の前後で値が一致していれば、増減が無いのでtrueを返します。値が不一致であれば増減があったので、falseを返します。OrderSelect関数も結果をチェックし、失敗していればfalseを返して即終了するようにしました。

    getTotalProfit関数のサンプル2

    bool getTotalProfit(int magic, double &profit_total)
    {
      profit_total = 0;
      int orders_total = OrdersTotal();
    
      for(int i = 0; i < orders_total; i++) {
          if(!OrderSelect(i, SELECT_BY_POS)) return(false);
          if(OrderMagicNumber() != magic) continue;
          if(OrderSymbol() != Symbol()) continue;
    
          profit_total += OrderProfit();
      }
    
      return(orders_total == OrdersTotal());
    }
    

     サンプル2のコードであれば、返り値でオーダーの増減を捉えることができます。増減があった際には正しい結果が取得できない可能性がありますので、falseの場合にはリトライするような処理を呼び出し元で実装しましょう。これによってサンプル1の問題は解決しました。

 ではサンプル2は完璧な処理と言えるでしょうか?

 答えはノーです。

 トレーディングプールにオーダーが追加・削除されるタイミングは他にも2通りありました。「部分決済」が起きた時と、「待機注文のエントリー」が起こった場合です。この2通りは、オーダーの数は変わりませんが、インデックスの並び替えが発生しています。そのためOrdersTotalの値が変わらないので、現在の処理では検知できません。
 一つ例を見てみましょう。下の図はindex 2のポジションが半分だけ決済された場合の結果です。 半分決済された場合のインデックス変化の例

半分決済された場合のインデックス変化の例

 上の図のような変化が、index 2のオーダーが選択された直後に起きた場合はどうなるでしょうか?index 0と1は通常通り処理が完了し、index 2についても1.0ロットのときの損益が計算されます。次にindex 3が計算されますが、部分決済が起きたことでオーダーの並びが変わるため、この時計算されるのは元index 4のオーダーになります。続いてindex 4のオーダーが計算されますが、これは部分決済されて0.5ロットになった、元index 2のオーダーになります。
 この時の損益は次のようになります。

4,000 - 6,000 + 8,000 - 3,000 = +3,000

 部分決済される前の損益と、部分決済された後の損益が2重で計算されているため、結果がズレています。結果のズレは待機注文がエントリーした場合も発生する可能性があります。また、サンプル2の処理ではインデックスを昇順に参照していますが、降順でもズレるときがあります。OrderSelect関数を使った処理は、途中でインデックスの並びが変わってしまうと、正しい結果を取得できないのです。
 ではどのような処理であれば正しい結果を取得できるでしょうか?再び考えてみましょう。サンプル2が失敗するのは、部分決済や待機注文のエントリーが原因でした。部分決済や待機注文のエントリーの発生を検知することができれば、返り値にfalseを返すことができそうです。「ヒストリープールにオーダーが追加されるタイミング」を思い出してください。部分決済がされた時と、待機注文がエントリーした時には、どちらもヒストリープールにオーダーが追加されます。そこで処理の前後で、OrdersHistoryTotalの値もチェックしてみましょう。トレーディングプールとヒストリープール内のオーダー両方が一致している場合のみtrueを返します。どちらかのプールが変化している場合は、何らかの取引が発生し、インデックスの並びが変わった可能性がありますので、falseを返します。

getProfitTotal関数のサンプル3

bool getTotalProfit(int magic, double &profit_total)
{
    profit_total = 0;
    int orders_total = OrdersTotal();
    int orders_history_total = OrdersHistoryTotal();

    for(int i = 0; i < orders_total; i++) {
        if(!OrderSelect(i, SELECT_BY_POS)) return(false);
        if(OrderMagicNumber() != magic) continue;
        if(OrderSymbol() != Symbol()) continue;

        profit_total += OrderProfit();
    }

    return(orders_total == OrdersTotal() && orders_history_total == OrdersHistoryTotal());
}

 サンプル3のコードは大丈夫でしょうか?実はこのコードでもまだ失敗する可能性があります。
 処理中にヒストリープールのオーダーが減ってしまうと問題がありそうです。ヒストリープールのオーダー数が減少するタイミングが、一つだけありましたね。口座履歴の期間を変更した場合です。もし口座履歴の変更がどのような影響があるか忘れてしまった方は、「ヒストリープールからオーダーが削除されるタイミング」で復習してください。サンプル3のgetProfitTotal関数は、OrderSelectを使ったループ中に、次の現象が重なると失敗します。

  • 次の①~③のいずれかが発生
    ①部分決済
    ②待機注文のエントリー
    ③エントリーと同数の決済
    (トレーディングプールのオーダー増減が0)
  • 口座履歴の期間を操作
  • 口座履歴変更後のヒストリープールのオーダー数が、変更前と等しい
    (ヒストリープールのオーダー増減が0)

 ではこの問題はどう処理すれば解決できるでしょうか?実はこのパターンに関しては対処するのが難しいです。これ以上厳密に判定しようとすると、ループ処理の前段階でオーダープール内の情報を取得して、ループ後に変化が無いか比較する必要があります。しかしオーダープール内の情報を取得するには、OrderSelect関数を使用した処理が必要になってしまい、結局前段階で取得したデータが正しいのかをどうやってチェックするのかという話になってしまいます。
 ヒストリープールの件数を使用する方法は実装が楽ですが、極僅かに失敗する可能性が残ってしまいます。ヒストリープールの値は使わずに判断する方法を考える必要があります。

getProfitTotal関数のサンプル4

bool getTotalProfit(int magic, double &profit_total)
{
    profit_total = 0;
    int orders_total = OrdersTotal();
    int ticket = OrderSelect(orders_total - 1, SELECT_BY_POS) ? OrderTicket() : -1;
    if(ticket == -1) return(true);

    for(int i = 0; i < orders_total; i++) {
        if(!OrderSelect(i, SELECT_BY_POS)) return(false);
        if(OrderMagicNumber() != magic) continue;
        if(OrderSymbol() != Symbol()) continue;

        profit_total += OrderProfit();
    }

    if(!OrderSelect(orders_total - 1, SELECT_BY_POS)) return(false);

    return(orders_total == OrdersTotal() && ticket == OrderTicket());
}

 ヒストリープールを使用せず、サンプル2を参考に改良を加えたサンプル4です。ループ処理を実行する前に、インデックスが一番大きい値のチケット番号を取得しています。処理後にチケット番号を再度比較することで、オーダーの増減があったかを判断します。ちなみに処理前の段階でOrderSelectが失敗する場合は、口座には一つもオーダーが無いということなので、return(true)を実行しています。
 もし処理中に決済や新規エントリーが発生しても問題はありません。その場合はOrdersTotal()の件数が変わるため、検知してfalseを返します。ここはサンプル2と同じです。
 問題はサンプル2で対応できなかった、部分決済、待機注文のエントリー、エントリーと決済が同数だった場合の3パターンです。
 この3パターンはいずれも新しいチケット番号のオーダーが追加されます。そのためチケット番号をチェックすることでこれらの3パターンが発生したかを検知可能です。
 部分決済の場合は、既存のオーダーは一度無くなり、インデックス番号が一番大きいオーダーとして新たに追加されます。(詳しくは「トレーディンプールにオーダーが追加されるタイミング」を参照)
 このため処理中に部分決済が発生していると、処理の前後で一番インデックスが大きいチケット番号が変わります。これを利用して検知するのがサンプル4の方法です。
 待機注文のエントリーも同様です。エントリーと決済が同数だった場合も検知可能ですが、処理中にエントリーが発生し、その新規ポジションが即決済となった場合だけ検知できません。ただしこの場合、他のEAもしくは手動での取引です。損益計算に関係の無いポジションですし、初期のインデックスの並びに影響を与えないため検知できなくとも問題ありません。むしろfalseを返して再計算を要求するよりも、trueを返してくれた方が良いでしょう。
 長くなりましたが、このサンプル4が私のオススメする最も堅牢なOrderSelectの方法です。

総括

 なんでこんなに面倒な処理を考えなきゃいけないのか?どうしてこんな稀なケースを考えなければいけないのか?そう思った人も多いでしょう。私も以前はサンプル1程度のチェックしかしていない処理で運用をおこなっていました。しかし1口座で複数のEAを稼働していた際、今回紹介したような不具合に出会いました。OrderSelect関数のループ処理中に他EAの決済がおこなわれてしまい、結果として本来ロジックに無い場面での決済が発生したのです。

 それ以来私はオーダープールとインデックスに関する正しい知識を身につけるため、様々なケースでインデックスの変化を観察しました。このミスが私一人の知識不足による失敗で終わればそれはそれで良かったのかもしれません。しかし世の中に出回っているEAや、その開発者を見ていると、どうも正しい理解ができていないのは私一人では無いような気がしてきました。そこで共有のために私の知識をまとめたのがこの章になります。

 2章ではトレーディングプールから合計損益を計算する処理を例に紹介してきました。最終的にはサンプル4のような処理となりましたが、処理の内容が変わってくると、サンプル4の方式では問題が発生してきます。例えばループの中でOrderCloseやOrderDeleteを使う場合、iを昇順で処理するサンプル4の方法では問題が発生します。OrderSelect関数を使う際は、処理に応じて臨機応変にコードを書く必要があります。そのためには2章で述べたような、オーダープールにオーダーが追加・削除されるタイミングを熟知している必要があります。

 ただし必ずしも不具合が出ないコードを書かなくてはいけない訳ではありません。どのような使い方をすると不具合が出るのか分かっているのであれば、その使い方を避ければいいのです。いわゆる「運用でカバー」する方法です。EAによっては処理速度を重視したい場合もありますし、自動売買において運用でカバーする方法は十分有りだと思います。実は2章で出てきた問題は、複数のEAを使ったり、手動で取引をするから発生する問題です。「1口座1EA」さえ守るのであれば、サンプル4のような長々として処理を書く必要はありません。

 EAを販売したり譲ったりする場合には、開発者としてどういう使い方をすると不具合を発生するのか、ユーザーに伝えてあげる必要があります。大半の利用者は不具合は無いものと考えています。そして自由に様々使い方でEAを利用するので、正しい運用方法を伝えないと不具合が生じます。

 総括がだいぶ長くなってしまいました。まとめると以下のようになります。

  1. オーダープールとインデックスについて詳しく理解しましょう
  2. 可能な限り不具合のでない実装をおこないましょう
  3. どうしても不具合が出るケースは運用面でカバーしましょう
  4. 不具合の出るケースはユーザーにも伝えましょう
  5. 2章の内容が良くわからない場合は1口座1EAを守りましょう
arrow_upward