MT4におけるバックテストと実際の運用の違い

 作成したEAを長期間のバックテストにかけて、もしそれがとても良い成績だったなら、すぐにでも運用してみたくなりますよね。しかしバックテスト結果を信用して、いきなり本番口座で実践運用するのはとても危険な行為です。実はバックテストによる動作確認には、大きな落とし穴が存在しています。

  • 再起動がない
  • 絶対に滑らない注文
  • スプレッドが固定値

この記事では上記の3点について解説していきます。

バックテストには再起動がない

 バックテストで得られた成績は、テスト期間中に一度も再起動が起こらなかった場合の成績です。過去10年分のバックテストであれば、「10年間再起動せずに運用できたらこの成績になりますよ」という成績です。

 果たして世の中に、10年以上一度もMT4を再起動していない人なんているのでしょうか?
あるいは今後10年間、一度もMT4を再起動しないで運用できる人はいるのでしょうか?
私はいないと思います。

再起動の問題点

 では実際にMT4が再起動されるとどのような問題があるのでしょうか?
それは外部変数(関数の外で宣言された変数)が初期化されてしまうことにあります。

 例として、ポジション管理をエントリー時に返却されるチケット番号でおこなっているEAの処理を見てみます。

例えば以下のコード

int ticket;

void OnTick()
{
    if(ticket == 0) {
        ticket = OrderSend(Symbol(), OP_BUY, lots, Ask, slippage, sl, tp, comment, magic);
    }
}

 上のコードのように、外部変数にチケット番号を保持しておいて、決済シグナルが発生したらチケット番号を使用して決済するEAがたくさんあります。(残念なことにこの方法が使われている書籍まであります。)しかしこの処理方式のEAはポジション保有中にMT4が再起動されるとアウトです。

 再起動が発生すると外部変数ticketが初期化されて値が0になるため、それまで保持していたチケット番号は失われます。これによりEAは自分がポジションを持っていないと誤認します。

 その結果本来決済されるべきところでポジションが決済されずにいつもまでポジションを持ち続けたり、1ポジションしか持たないEAのはずが2ポジション目を持ちはじめたりします。こんなに恐ろしいバグが再起動しただけで発生してしまいます。そしてこのバグは、バックテストでは決して動作確認することができないのです。

 チケット番号に限らず、外部変数は再起動時に全て初期化されてしまいます。外部変数を使うときは細心の注意が必要なのです。

OnInitにリカバリー処理を入れよう

 実践で運用可能なものに改善するには、OnInit内で値のリカバリーをしてあげる必要があります。リカバリー処理とは、再起動前の値を適切に算出して値を代入する処理のことです。先ほどの例のコードに、リカバリー処理を加えると以下のコードのようになります。

input int Magic = 1;

int ticket;

int OnInit()
{
    int orders_total = OrdersTotal();
    int check_ticket = -1;

    if(orders_total > 0) {
        if(!OrderSelect(orders_total - 1, SELECT_BY_POS)) return(INIT_FAILED);
        check_ticket = OrderTicket();
    }

    if(ticket != -1) {
        for(int i = 0; i < orders_total; i++) {
            if(!OrderSelect(i, SELECT_BY_POS)) return(INIT_FAILED);
            if(OrderMagicNumber() != Magic) continue;
            if(OrderSymbol() != Symbol()) continue;

            ticket = OrderTicket();
            break;
        }

        if(!OrderSelect(orders_total - 1, SELECT_BY_POS)) return(INIT_FAILED);
        if(orders_total != OrdersTotal() || check_ticket != OrderTicket()) {
            return(INIT_FAILED);
        }
    }


    return(INIT_SUCCEEDED);
}

void OnTick()
{
    if(ticket == 0) {
        ticket = OrderSend(Symbol(), OP_BUY, lots, Ask, slippage, sl, tp, comment, magic);
    }
}

上のコードでは、OnInit内で各ポジションのチェックをおこなっており、マジックナンバーを使って以前このEAが保有していたポジションを探します。一致するポジションがあれば、そのポジションのチケット番号を使用して、外部変数ticketに値をセットします。このように適切なリカバリー処理を施すことで、再起動が発生しても停止前と同じ状況で運用を再開することが可能です。

 他の方法としては、csvなどのファイルに外部変数の値を出力しておき、起動時に読み込む方法もあります。しかしこの方法は停止前に正しくcsvファイルを出力されていることが前提です。もし何らかの理由で正しく出力されていなかった場合には、誤った情報で運用が再開される危険性があるため、あまり良い方法ではないでしょう。

外部変数はなるべく避けよう

 外部変数には重要なデータは保持せず、容易にリカバリー可能なデータだけを保持するのが良いでしょう。例に挙げてきたチケット番号の場合は、リカバリー処理もけっこう手間ですので、外部変数にしないほうが望ましいです。チケット番号のは外部変数で保持せずとも、必要な場面で都度取得する形にすることで、外部変数を減らすことができます。

バックテストの注文は滑らない

 バックテストの場合、TP/SLおよび待機注文は、発注した価格で確実に約定します。これにより、実際の成績とバックテストの成績が乖離する可能性があります。

TPとSL

下の例で説明します。

  • 110円で買い
  • TPを111円、SLを109円に設定
  • 週末に110.500円でマーケットがクローズ
  • 窓が開き、112円でマーケットオープン

さて、この場合の損益はどうなるでしょうか?

 窓が開いたことで、マーケットがオープンした時点で利食いとなります。実際の取引であれば、マーケットが112円ですので、買いポジションも112円で決済されます。そのため2円分の値幅が利益となります。

 バックテストの場合、決済価格は112円にはなりません。なんとこの場合、TPが設定されている111円で決済されてしまいます。そのため利益は1円分の値幅となり、実際の利益よりも1円分少なくなってしまいます。

 下の画像は、2017/04/21(金)〜2017/04/24(月)にかけて、ドル円(5分足)にて大きな窓が開いたときのチャートです。この画像では、週末のマーケットクローズ直前に買いエントリーをおこないました。TPを+1円に設定してバックテストした事例です。
バックテストのTPが正しく機能しなかった事例

バックテストのTPが正しく機能しなかった事例

 おかしな価格で決済されているのがお分かりいただけたでしょうか?この月曜日のマーケットオープンは、110.571円でしたので、この価格前後で決済されるのが正しい成績です。しかし実際には、TPが設定されていた110.161円でピッタリと決済されています。

同様のことは、SLでも発生します。マーケットオープンが仮に98円であったとしましょう。その場合、実践運用では2円分の損失ですが、バックテストは1円分の損失となります。

 このようにMT4のバックテストは、TP/SLで設定した価格で決済となるのです。どうやらテスト時の価格は、TP/SLの条件を満たしているかの判定にしか使われていないようです。TPの場合バックテストは本来よりも不利な成績となり、SLの場合バックテストは本来よりも有利な成績となります。

 わかりやすく説明するために極端な例を出しましたが、これは普通の場面でのTP/SLでも同様です。ドル円が毎回必ず0.001円ずつ動くわけではないので、0.1pipsや0.2pips程度の小さな誤差は頻繁に発生している可能性があります。

 通常はTPによるマイナスの誤差とSLによるプラスの誤差が混ざるので、運用との差はほとんど無くなるかもしれません。しかし、SLは設定しているがTPは使用していないEAの場合、SLのプラスの誤差だけが混入します。これによりバックテストの成績が実際に運用した場合の成績よりも良い結果になっている可能性があります。(TP/SLの割合、取引したタイミング、ブローカーごとの値動きの違いなどによって変わるため、絶対に間違っているとは言い切れません。)

待機注文

 TPとSLと同じ問題は、待機注文でも発生します。先に断っておきますと、私はTP/SLと待機注文という言葉を分けて使用しています。実際にはTP/SLも待機注文と呼ぶのが正しいかもしれませんが、私が待機注文を使うときには、以下の4種類を指しています。

  • 指値買いエントリー注文
  • 逆指値買いエントリー注文
  • 指値売りエントリー注文
  • 逆指値売りエントリー注文

 下の画像は、先程と同じ2017/04/21(金)〜2017/04/24(月)のチャートです。マーケットクローズ直前に、買いでエントリーする代わりに1円上に逆指値買い注文を出した場合の結果です。
バックテストの逆指値買いが正しく機能しなかった事例

バックテストの逆指値買いが正しく機能しなかった事例

 こちらもおかしな価格でエントリーされているのがお分かりいただけるかと思います。本来はマーケットオープン時の110.571円でエントリーとなるべきですが、注文を入れた110.161円でエントリーしています。このように、待機注文を使用した場合にも実際の取引とズレが発生してしまいます。逆指値買いまたは逆指値売りを使用した場合は実際よりも有利な価格でエントリーとなる可能性があります。反対に、指値買い、指値売りを使用した場合は実際よりも不利な価格でエントリーとなる可能性があります。

誤差があることを認識してテストしよう

 この誤差の問題は、TP/SLと待機注文が均等に使われていれば、それほど大きな誤差とはなりません。均等に使用されている場合、プラスの誤差とマイナスの誤差が入り混ざることで、誤差は0を挟んで揺れ動くはずだからです。しかし、例えばSLだけ使用していて、利食いは成り行きのEAの場合は注意が必要です。SLを使用した場合、バックテストの決済価格はフォワードテストの決済価格よりも有利な価格となることがあります。取引回数が増えるに連れて、損失が縮小した取引が少しずつ積み上がっていくこととなります。これにより実際の成績よりも良いバックテスト結果となっている可能性があります。

 この問題を解決するための最も単純な解決策は、全て成り行きで取引することです。全ての取引を成り行きにすることは、多少処理を複雑にしますが、不可能なことではありません。しかしTP/SLや待機注文を全く使わないというのは、現実的な手法ではないでしょう。相場の急騰・急落に備えて、TP・SLを設定したい人は多いと思います。TP/SLや待機注文を使用したバックテストでは誤差が発生することを正しく認識していれば、成り行き注文にこだわる必要は無いと著者は考えております。この問題を知らぬまま、SLや逆指値注文を多用したことで本来の成績よりも良い成績のバックテスト結果を鵜呑みにして運用を開始することが危険なのです。

バックテストはずっとスプレッドが固定値

実践取引ではスプレッドは常に変動します。指標発表、要人発言、朝方などの流動性の低い時間帯などです。固定スプレッドを謳っているブローカーであっても、重要指標のときなどはスプレッドが開いてしまいます。理想を言えば、MT4のバックテストも実際のスプレッド変動に合わせて、スプレッドを変動させてテストされるべきでしょう。しかしMT4のバックテストは常に一定のスプレッドでバックテストが実施されてしまうのが現実です。
 これによってリアル運用とバックテストの成績の間には差が生まれてしまいます。そこで開発者はこの理想と現実の差を埋めるため、処理を工夫する必要があります。

 スプレッドをどの値でテストするかは、バックテスト前に設定可能です。
バックテスト時のスプレッドの設定方法

バックテスト時のスプレッドの設定方法

スプレッドフィルターを実装しよう

 実際の取引ではバックテスト時のスプレッドよりもさらに広がる場面があります。例えば大きな指標発表などがそうです。このような場面でバックテスト通り取引してしまうと、不利なトレードを強いられることになってしまいます。そこでスプレッドフィルターを実装するのをおすすめいたします。スプレッドフィルターは、スプレッドが一定以上広がった場合に、取引を制限させるための機能です。

 実装の例を紹介します。今回はSpreadFilter関数を作成して判定を行いたいと思います。引数にmax_spreadを渡して、この値より大きいか小さいかで判定を変えます。スプレッドがmax_spread以下なら取引可能ということでtrueを返します。もしスプレッドがmax_spreadより大きい場合は、取引不可能ということでfalseを返します。

bool SpreadFilter(const int max_spread)
{
    //max_spreadはpoint単位なので注意
    return(MarketInfo(_Symbol, MODE_SPREAD) <= max_spread);
}

 現在のスプレッドはMarketInfo関数の第二引数に、MODE_SPREADを指定することで取得できます。スプレッドはpoint単位の値として取得されます。上記の例はmax_spreadをpoint単位の値として受け取った場合の処理です。max_spreadをpips単位で指定したい場合は、関数内で係数をかけて調整してください。

 スプレッドフィルターは実際に多くのEAで実装されている機能です。万が一テスト時のスプレッドよりも広がっても、スプレッドフィルターにより不利な取引を避けることが可能となります。ただしそのため、実際の運用時にはバックテスト時よりも取引回数が減る可能性があります。

広めのスプレッドでテストしよう

 スプレッドフィルターによって頻繁に取引が制限されてしまうと、期待した利益が得られません。そこでバックテストする際には、普段よりも少し広めのスプレッドに設定しておきましょう。
これによりスプレッドフィルターの設定も広めにすることができるので、取引が制限される頻度が減ります。スプレッドを広げすぎてしまうと、良い成績のEAを作るのは難しくなってしまいますので、広げ過ぎには注意です。

総括

 この章ではバックテストの結果で気をつける箇所を紹介しました。実際の取引時と異なる点が幾つかあるため、バックテストが上手くいっても油断しないようにしましょう。この章の内容をまとめると以下のようになります。

再起動への対策

外部変数は再起動時などに初期化されるため、実際の運用では処理に影響が出る可能性がある。
この挙動はバックテストでは確認できない。
そのため外部変数はなるべく使わない方が良い。
削れない外部変数に関しては、OnInit関数内でリカバリー処理を入れ、正しく処理できているかをフォワードテストで確認する。

注文の誤差に注意する

バックテストのTP/SL、待機注文は、注文した値段で約定してしまう。
これにより誤差が発生し、実際の成績よりもよく見えることがある。 もちろん逆に悪く見えることもある。
バックテストする際にはこのことを頭に入れておき、TPだけ、SLだけのEAの場合は成績に注意を払う。
可能であれば成り行き注文を使うことで、バックテスト時の誤差を気にしなくて済む。

スプレッドの差に注意する

バックテストは常に固定のスプレッドで取引となる。
バックテストのスプレッドは、通常時よりも少し広めに設定しておくのが良い。
急激なスプレッド変動に備えて、スプレッドフィルター処理を実装する。
運用時のスプレッドフィルターの値は、バックテスト時のスプレッド以下の値を設定する。

arrow_upward