【M4】サインツールのバックテスト方法解説!FX・バイナリー攻略用

MT4バックテストのやり方 サインツール

FXやバイナリーで使える【M4・MQL4】サインツールのバックテスト方法をわかりやすく解説。サインが出た次のローソク足で勝敗を判定し、計算処理だけで効率よく検証できます。さらに、10年分のヒストリカルデータを使って、正確な勝率を導き出す手順も紹介しています。

このページで分かる事
  • サインツールの基本的なバックテスト方法
  • サインが出た「次のローソク足」で勝敗を判定する仕組み
  • 計算処理内で完結する効率的なバックテストのやり方
  • 勝率をエキスパートタブに表示する方法
  • 10年分のヒストリカルデータを使った検証方法
  • Dukascopy・FXDDのデータ取得状況と代替手段(Axioryの活用法)
  • MT4ヒストリーデータの精度問題と注意点
  • PeriodConverterを使った時間足データの作成方法

サインツールのバックテストをどの様に行うべきか

サインツールのバックテストのやり方は色々ありますが、今回は計算処理内で全て済ませるやり方を紹介します。

まず前提として、サインツールの多くはサインが出た次のローソク足にエントリーします。

その為、サインが出た「次のローソク足の値を取得」できれば勝率を導き出す事が出来ます。

バックテストのサイン

今回のコードでは±3σにタッチした際にサイン(矢印)が出来ます。

また、サイン(矢印)の次のローソク足も見ており、勝ち判定の場合は「笑顔のマーク」、負け判定なら「悲しい顔のマーク」を表示させています。

このツールは反発を狙ったシグナルツールなので、サイン(矢印)が出た次のローソク足が反対方向に動けば「勝ち」と判定します。

例えば、サイン(矢印)が出たローソク足が陽線で、次のローソク足が陰線であれば勝ち判定、次のローソク足が陽線であれば負け判定となります。

サインの次の足の勝ち負けをハッキリする事が出来れば勝率を出す事が可能です。

エキスパートに勝率を表示

勝率はターミナルの「エキスパート」部分に表示させます。

判定する箇所がハッキリしていれば、何年分でも測定が可能で勝率を確認する事が出来ます。

【バックテスト付き】サインツールのコードを紹介

まずは以下のコードをコピーして動作確認を行って下さい。


#property indicator_chart_window
#property indicator_buffers 9

#property indicator_color1 Red        // 売りサイン ▼
#property indicator_color2 Blue       // 買いサイン ▲
#property indicator_color3 Red        // 勝ちサイン
#property indicator_color4 Blue       // 負けサイン
#property indicator_color5 Green      // Middle Band 2σ
#property indicator_color6 Orange     // Upper Band 2σ
#property indicator_color7 Orange     // Lower Band 2σ
#property indicator_color8 Magenta    // Upper Band 3σ
#property indicator_color9 Magenta    // Lower Band 3σ

// バッファ定義
double SellArrowBuffer[];
double BuyArrowBuffer[];
double WinBuffer[];
double LoseBuffer[];
double MiddleBand2[];
double UpperBand2[];
double LowerBand2[];
double UpperBand3[];
double LowerBand3[];

// パラメーター
extern int BB_Period = 20;
extern double BB_Deviation2 = 2.0;
extern double BB_Deviation3 = 3.0;

extern double OffsetRatio = 0.1;
extern int MinOffsetPoints = 20;

// オフセット比率を時間足によって動的に変える関数
double GetOffsetRatio() {
    switch (Period()) {
        case PERIOD_M1:   return 0.5;
        case PERIOD_M5:   return 0.5;
        case PERIOD_M15:  return 0.5;
        case PERIOD_H1:   return 0.5;
        case PERIOD_H4:   return 0.5;
        case PERIOD_D1:   return 1.0;
        default:          return OffsetRatio;
    }
}

// インジケーター初期化
int OnInit() {
    SetIndexBuffer(0, SellArrowBuffer);
    SetIndexStyle(0, DRAW_ARROW, EMPTY, 2);
    SetIndexArrow(0, 226); // ▼

    SetIndexBuffer(1, BuyArrowBuffer);
    SetIndexStyle(1, DRAW_ARROW, EMPTY, 2);
    SetIndexArrow(1, 225); // ▲

    SetIndexBuffer(2, WinBuffer);
    SetIndexStyle(2, DRAW_ARROW, EMPTY, 2);
    SetIndexArrow(2, 74);

    SetIndexBuffer(3, LoseBuffer);
    SetIndexStyle(3, DRAW_ARROW, EMPTY, 2);
    SetIndexArrow(3, 75);

    SetIndexBuffer(4, MiddleBand2); SetIndexStyle(4, DRAW_LINE, STYLE_SOLID, 1);
    SetIndexBuffer(5, UpperBand2);  SetIndexStyle(5, DRAW_LINE, STYLE_DOT, 1);
    SetIndexBuffer(6, LowerBand2);  SetIndexStyle(6, DRAW_LINE, STYLE_DOT, 1);
    SetIndexBuffer(7, UpperBand3);  SetIndexStyle(7, DRAW_LINE, STYLE_DASH, 2);
    SetIndexBuffer(8, LowerBand3);  SetIndexStyle(8, DRAW_LINE, STYLE_DASH, 2);

    ArrayInitialize(SellArrowBuffer, EMPTY_VALUE);
    ArrayInitialize(BuyArrowBuffer, EMPTY_VALUE);
    ArrayInitialize(WinBuffer, EMPTY_VALUE);
    ArrayInitialize(LoseBuffer, EMPTY_VALUE);

    return (INIT_SUCCEEDED);
}

// メインロジック
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[])
{
    // 必要バー数が足りなければ終了
    if (rates_total < BB_Period) return 0;

    // 計算範囲を設定(全バー対象)
    int limit = rates_total - prev_calculated;

    if (prev_calculated == 0) {
        limit = rates_total - 1;
    }

    int wins = 0;
    int losses = 0;
    int signals = 0;

    int lastSignal = 0; // 1 = Buy, -1 = Sell, 0 = None

    double ratio = GetOffsetRatio();
    double minOffset = Point * MinOffsetPoints;

    // メインループ(最新から過去方向に計算)
    for (int i = limit; i >= 1; i--) {
        // ボリンジャーバンド計算
        double middleBB2 = iBands(NULL, 0, BB_Period, BB_Deviation2, 0, PRICE_CLOSE, MODE_MAIN, i);
        double upperBB2  = iBands(NULL, 0, BB_Period, BB_Deviation2, 0, PRICE_CLOSE, MODE_UPPER, i);
        double lowerBB2  = iBands(NULL, 0, BB_Period, BB_Deviation2, 0, PRICE_CLOSE, MODE_LOWER, i);

        double upperBB3  = iBands(NULL, 0, BB_Period, BB_Deviation3, 0, PRICE_CLOSE, MODE_UPPER, i);
        double lowerBB3  = iBands(NULL, 0, BB_Period, BB_Deviation3, 0, PRICE_CLOSE, MODE_LOWER, i);

        // バンドをバッファに格納(ライン描画用)
        MiddleBand2[i] = middleBB2;
        UpperBand2[i]  = upperBB2;
        LowerBand2[i]  = lowerBB2;
        UpperBand3[i]  = upperBB3;
        LowerBand3[i]  = lowerBB3;

        // 初期化(矢印をリセット)
        BuyArrowBuffer[i] = EMPTY_VALUE;
        SellArrowBuffer[i] = EMPTY_VALUE;
        WinBuffer[i - 1] = EMPTY_VALUE;
        LoseBuffer[i - 1] = EMPTY_VALUE;

        // ダイナミックオフセット計算
        double candleRange = high[i] - low[i];
        double dynamicOffset = candleRange * ratio;

        if (dynamicOffset < minOffset) {
            dynamicOffset = minOffset;
        }

        // シグナル判定
        bool sellSignal = close[i] >= upperBB3;
        bool buySignal  = close[i] <= lowerBB3;

        // 売りシグナル処理
        if (sellSignal && lastSignal != -1) {
            SellArrowBuffer[i] = high[i] + dynamicOffset;
            signals++;
            lastSignal = -1;

            if (close[i - 1] < open[i - 1]) {
                WinBuffer[i - 1] = low[i - 1] - dynamicOffset;
                wins++;
            } else {
                LoseBuffer[i - 1] = high[i - 1] + dynamicOffset;
                losses++;
            }

        // 買いシグナル処理
        } else if (buySignal && lastSignal != 1) {
            BuyArrowBuffer[i] = low[i] - dynamicOffset;
            signals++;
            lastSignal = 1;

            if (close[i - 1] > open[i - 1]) {
                WinBuffer[i - 1] = low[i - 1] - dynamicOffset;
                wins++;
            } else {
                LoseBuffer[i - 1] = high[i - 1] + dynamicOffset;
                losses++;
            }

        } else if (!sellSignal && !buySignal) {
            lastSignal = 0; // シグナル無しに戻す
        }
    }

    // 勝率計算
    double winRate = (signals > 0) ? (double)wins / signals * 100.0 : 0;

    // 初回のみレポート出力
    if (prev_calculated == 0) {
        Print("========== 3σシグナル 勝率レポート ==========");
        PrintFormat("シグナル回数: %d", signals);
        PrintFormat("勝ち回数: %d", wins);
        PrintFormat("負け回数: %d", losses);
        PrintFormat("勝率: %.2f%%", winRate);
    }

    return rates_total;
}


OnCalculate()の中身を徹底解説

OnCalculate()の中身をプログラム初心者向きに徹底的に解説しています。OnCalculate()の中身以外は関連記事で1から10まで紹介しています。

【徹底解説】MQL4ボリンジャーバンドのサインツールの作り方!FXやバイナリーを攻略
FXやバイナリーオプション攻略に使えるボリンジャーバンドのサインツールの作り方を徹底解説しています。紹介しているサインツールでは、±3σにタッチしたらサインが表示されアラートが発生します。
データが足りないときは終了

if (rates_total < BB_Period) return 0;

BB_Period(ビー・ビー・ピリオド) は、ボリンジャーバンドを計算するために使う、過去のローソク足(バー)の本数を指定するパラメータです。

例えば、BB_Periodが20 の場合、最新のローソク足を含めた 過去20本分 のデータを使ってボリンジャーバンドを計算します。

もし、チャート上のローソク足の本数(rates_total)が BB_Periodより少ない場合、計算に必要なデータが不足しているため、インジケーターの処理は実行されず、その時点で終了します。

どこから計算を始めるかを決める処理

int limit = rates_total - prev_calculated;

if (prev_calculated == 0) {
    limit = rates_total - 1;
}

このコードは、「過去のどこから、現在のどこまでのローソク足を計算するか」を決めています。

インジケーターを初めて表示したとき(prev_calculated == 0)は、一番古いローソク足から最新の1本前までを全部計算します。

それ以降は、前回計算した続きから、新しく追加されたローソク足までを効率よく計算しています。

項目 説明
rates_total 全部のローソク足の数
prev_calculated 前回までに計算が終わった本数
limit 今回どこまで計算するか決める数字
prev_calculated == 0 初めてチャートを表示したとき。全データを計算する(過去全部)
カウンターや記録用の変数

int wins = 0;
int losses = 0;
int signals = 0;
int lastSignal = 0; // 直前のサインを覚えておく

勝ち数、負け数、シグナル回数をカウントする変数と、前のバーで出したシグナルを記録し、同じシグナルが連続しないようにする lastSignal を使います。

オフセットの比率を計算

double ratio = GetOffsetRatio();
double minOffset = Point * MinOffsetPoints;

矢印をどのくらいずらすかは倍率で調整します。通貨ペアや時間足によって最適な距離が異なるため、その倍率は関数の中で設定します。また、距離の基準には通貨ペアの最小単位である Point を使います。

矢印の位置調整に関しては関連記事で詳しく解説しています。

【徹底解説】MQL4ボリンジャーバンドのサインツールの作り方!FXやバイナリーを攻略
FXやバイナリーオプション攻略に使えるボリンジャーバンドのサインツールの作り方を徹底解説しています。紹介しているサインツールでは、±3σにタッチしたらサインが表示されアラートが発生します。
ローソク足を1本ずつ調べる(for文)

for (int i = limit; i >= 1; i--) {

iはローソク足の番号で、limitは今回計算を開始する位置を示し、そこから1まで1本ずつ過去に遡って処理します。

limtは「limit = rates_total - prev_calculated;」で設定されています。

rates_total はチャートに表示されているすべてのローソク足の本数、prev_calculated は前回までに計算が終わったローソク足の本数を表します。

limit = rates_total - prev_calculated; という計算で、「前回の続きから、今回どこまで新しく計算するか」を決めます。

これによって、すでに計算済みのデータを無駄に処理せず、新しく追加された部分だけを効率よく処理できます。

ボリンジャーバンドの計算

double middleBB2 = iBands(NULL, 0, BB_Period, BB_Deviation2, 0, PRICE_CLOSE, MODE_MAIN, i);
double upperBB2  = iBands(NULL, 0, BB_Period, BB_Deviation2, 0, PRICE_CLOSE, MODE_UPPER, i);
double lowerBB2  = iBands(NULL, 0, BB_Period, BB_Deviation2, 0, PRICE_CLOSE, MODE_LOWER, i);

double upperBB3  = iBands(NULL, 0, BB_Period, BB_Deviation3, 0, PRICE_CLOSE, MODE_UPPER, i);
double lowerBB3  = iBands(NULL, 0, BB_Period, BB_Deviation3, 0, PRICE_CLOSE, MODE_LOWER, i);

iBands() は、指定したローソク足のボリンジャーバンドの値(中央線・上のライン・下のライン)を取得するための関数です。

このコードでは、ボリンジャーバンドの 2σと3σの中央・上限・下限をそれぞれ計算し、middleBB2、upperBB2、lowerBB2、upperBB3、lowerBB3 にその値を入れています。

変数名 内容
middleBB2 2σの中央線
upperBB2 2σの上限ライン
lowerBB2 2σの下限ライン
upperBB3 3σの上限ライン
lowerBB3 3σの下限ライン
計算結果をバッファに格納(ライン描画用)

MiddleBand2[i] = middleBB2;
UpperBand2[i]  = upperBB2;
LowerBand2[i]  = lowerBB2;
UpperBand3[i]  = upperBB3;
LowerBand3[i]  = lowerBB3;

各ボリンジャーバンドの値(2σと3σの中央線・上限・下限)を、それぞれ対応するバッファに格納し、チャートにラインを描画できるようにしています。

矢印や勝敗のバッファを初期化

BuyArrowBuffer[i] = EMPTY_VALUE;
SellArrowBuffer[i] = EMPTY_VALUE;
WinBuffer[i - 1] = EMPTY_VALUE;
LoseBuffer[i - 1] = EMPTY_VALUE;

各バッファ(買い矢印・売り矢印・勝ち・負け)の表示をリセットするために、該当するインデックスに EMPTY_VALUE を設定しています。

矢印の高さ(オフセット)を計算

double candleRange = high[i] - low[i];
double dynamicOffset = candleRange * ratio;

if (dynamicOffset < minOffset) {
    dynamicOffset = minOffset;
}

ローソク足の高値と安値の差から値幅(candleRange)を計算し、それに倍率(ratio)をかけて矢印をずらす距離(dynamicOffset)を決め、最低値幅(minOffset)より小さい場合は minOffset を適用します。

サインを出す条件を判定

bool sellSignal = close[i] >= upperBB3;
bool buySignal  = close[i] <= lowerBB3;

現在のローソク足の終値が3σの上限を超えた場合は売りシグナル(sellSignal)、下限を下回った場合は買いシグナル(buySignal)として判定します。

売りサイン処理

if (sellSignal && lastSignal != -1) {
    SellArrowBuffer[i] = high[i] + dynamicOffset;
    signals++;
    lastSignal = -1;

「売りシグナル」が出たときに、直前のシグナルがまだ売りになっていない場合だけ、チャートに売り矢印を表示します。

これは、同じ売りシグナルを何度も連続して出さないための仕組みです。

その後、シグナルを出した回数を1つ増やして、「最後に出したのは売りシグナルだった」という情報を lastSignal = -1 として記録し、次の判断に使います。

売りサインの勝敗判定

if (close[i - 1] < open[i - 1]) {
    WinBuffer[i - 1] = low[i - 1] - dynamicOffset;
    wins++;
} else {
    LoseBuffer[i - 1] = high[i - 1] + dynamicOffset;
    losses++;
}

売りシグナルを出した次の足(i - 1)で、もし終値(クローズ)が始値(オープン)より下がっていれば、そのトレードは「勝ち」と判断し、勝ちの印(矢印など)を表示します。そして、勝ち回数を1つ増やします。

逆に、終値が始値より上がっていた場合は「負け」と判断し、負けの印を表示して、負け回数を1つ増やします。

表示位置は見やすいように dynamicOffset を使って上下に少しずらしています。

※買いサインの処理も同じ流れになります。

何もないときはシグナルをリセット

else if (!sellSignal && !buySignal) {
    lastSignal = 0;
}

「売りシグナル」も「買いシグナル」も出ていないときは、lastSignalを0に戻します。

これは、「今はシグナルが出ていない状態だよ」とリセットしておくことで、次に新しいシグナルが出たときに正しく判定できるようにするためです。

こうすることで、同じシグナルが何度も続けて出てしまうのを防いでいます。

処理が終わった後にやること

double winRate = (signals > 0) ? (double)wins / signals * 100.0 : 0;

まず、signals(シグナルが出た回数)が1回以上ある場合は、wins(勝った回数)をsignalsで割って、「勝率」を計算します。その結果に100をかけて、パーセント(%)で表します。

もし、まだシグナルが1回も出ていない場合は、勝率は計算できないので winRate を 0 にします。

つまり、「出したシグナルのうち、どれくらい勝ったのか」をわかりやすく%で表しているものです。

初回のみのレポート出力

    if (prev_calculated == 0) {
        Print("========== 3σシグナル 勝率レポート ==========");
        PrintFormat("シグナル回数: %d", signals);
        PrintFormat("勝ち回数: %d", wins);
        PrintFormat("負け回数: %d", losses);
        PrintFormat("勝率: %.2f%%", winRate);
    }

prev_calculated == 0 の場合、つまり「チャートを初めて表示したとき」だけ、コンソール(エキスパートタブ)に勝率レポートを出力します。

表示される内容は、シグナルの回数、勝った回数、負けた回数、そして勝率(パーセント)です。

この処理は一度だけ行われるので、毎回何度も同じ情報が表示されることはありません。

※PrintFormat() は「エキスパート(ターミナルのエキスパートタブ)」に出す関数

バックテストは意外と簡単にできる

バックテストの目的は、作成したサインツールの勝率を確認することです。

シグナルが出た回数(signals)と、そのうち勝った回数(wins)をカウントすれば、勝率は簡単に計算できます。

あとは、PrintFormat()を使って「シグナル数」「勝ち数」「負け数」「勝率」を表示するだけで、バックテスト結果をまとめることができます。

バックテストで最も大切なのは、勝ちと負けを正しく判定する部分です。ここさえしっかりしていれば、誰でも簡単にバックテストができます。

10年分のデーターでバックテストをする方法

ヒストリカルデータで
10年分のデータを解析

MT4のヒストリーデータ(過去データ)は1分足の場合、一部のブローカーは直近1〜2年分のみになっています。その為、バックテストをするにはデーターが足りないケースも多々あります。

長期間の過去データを使って検証する場合、「Dukascopy」や「FXDD」などからヒストリカルデータをダウンロードできると言われています。しかし、2025年3月現在は、これらのサイトからデータを取得することができません。

Dukascopyはダウンロードできますが、インポートがうまく来ません。また、FXDDはダウロード自体が出来なくなっています。

そこでおすすめなのが「AXIORY」のヒストリカルデータになります。導入まで、多少面倒なのですが、無料で10年分のデータを使う事が出来ます。

※MT4でもヒストリーデータのダウンロードは可能ですが、精度が低く、実際に使用してみると1970年からデータがあり、そのデータがおかしい表示をしています。

バックテストの結果にも悪影響がでるのでMT4のヒストリーデータは使用しない事をおすすめします。

Axioryのヒストリカルデータのダウンロード方法

※当サイトは海外FXを推奨していません。Axioryのヒストリカルデータのみ有効活用しましょう。

アキシオリーの検索

「Axiory ヒストリカルデータ」と検索すると「アキシオリーのMT4 ヒストリカルデータ | 今すぐ無料ダウンロード!」と言うタイトルが出てくるのでそちらをクリックします。

アキシオリー

アキシオリーのヒストリカルデータページでヒストリカルデータをダウンロードします。※1年分ごとにダウンロードする必要があります。

ヒストリカルデータをダウンロード

ダウンロードしたファイルを開くと月ごとのファイルと「all.csv」に分かれているので「all.csv」のみを集めます。

ヒストリカルデータをダウンロード(all)

2015年の「all.csv」~2025年の「all.csv」を一つのファイルにまとめておきます。※2018年や2019年は年数が書かれていないので画像の場合は2018をファイル名に変更します。

同じファイル名だと後々分からなくなります。

MT4のデータフォルダを開く

「ファイル」より「データフォルダを開く」を選択します。

MT4の「history」を選択

「history」を選択します。

MT4の口座選択

ヒストリカルデータを入れ替えたい口座を選択します。※MT4を使用している口座のフォルダを選びます。

ヒストリカルデータの削除

ヒストリカルデータを入れたい取引通貨のデータがあった場合は全て削除します。

今回はUSD/JPYのヒストリカルデータを入れたいのでUSD/JPYのデータを全て削除します。

ヒストリーセンターを選択

MT4の「ツール」より「ヒストリーセンター」を選択します。

取引したい通貨ペアの「1分足」を選択

取引したい通貨ペアの「1分足」を選択し、「インポート」ボタンをクリックします。

ダウンロードしたファイルを選択

参照より、ダウンロードしたファイルを選択し、「OKボタン」をクリックします。

10年分だと「all.csv」が10個あるのでこれを10回繰り返します。

MT4の再起動

「閉じる」ボタンをクリックしてMT4を再起動させます。

一番古い足の確認

MT4を立ち上げてヒストリカルデータを入れた1分足の一番古い足が2015年(10年分のデーターの場合)になっているか確認します。

※HOMEボタンを押し続けると一番古いデータまで飛んでくれます。

オフラインチャートの選択

MT4の「ファイル」より「オフラインチャート」を選択します。

インポートした1分足のデータを選択

インポートした1分足のデータを選択して「開く」ボタンをクリックします。今回の場合「USDJPY,M1」を選択しています。

オフラインチャートの確認

チャートに1分足のオフラインチャートが表示されます。

PeriodConverter

ナビゲーター内にある「スクリプト」の「PeriodConverter」を「USDJPY,M1(offline)」のチャート内にドラッグ&ドロップします。

PeriodConverterパラメーター入力

Script-PeriodConverterの画面が表示されるので「パラメーター入力」タブの「Period multipliter factor」の値を「5」にします。

今回は5分足を使いするので「5」としましたが、「5」「15」「30」「60」「1440(1日)」の様に必要に応じて追加していきます。

追加後、MT4を一度再起動します。

一番古いデータを確認

「USDJPY,M1(offline)」では通常のUSD/JPYの5分足チャートを開いて一番古いローソク足を確認します。10年間のデータの場合は一番古いデータが2015年になっているのか確認しましょう。

オフライン・チャートリストより確認

データの確認はオフライン・チャートリストより確認する事も出来ます。

綾瀬 文也

10年以上のFXトレード経験を持ち、チャート分析と戦略構築を専門とするトレーダー。マーケットの変動を的確に捉え、リスク管理を徹底することで安定した収益を実現。現在はpokyun企画代表として活動

綾瀬 文也をフォローする
サインツール
綾瀬 文也をフォローする
タイトルとURLをコピーしました