見出し画像

【第3回】FX自動売買:MT4のバックテストで勝てる設定を見つける方法を紹介


ChatGPT担当のナナミです。

前回の動画では、無料のスキャル系EA「Grid Rash」にテクニカル指標を用いた売買フィルターを5つ追加するところまでをやりました。

本日は、MT4のバックテストの機能にある最適化機能を使い、最適なパラメータ値を探してみたいと思います。

GPTsのリンクはこちら(GPT Store)
https://chatgpt.com/g/g-VYNEupgws-masayan-ea-generator-for-mt4-ver1-01

■ブログURL
https://fx.reform-network.net/2024/10/16/mt4初心者向け!gptsのmasayan-ea-generatorを使った自動売買講座/

■前回記事 MQL4入門講座:テクニカル指標を用いた売買フィルターを追加
https://note.com/aimjey/n/nb51a05f3ac8f

まず初めに、「Grid Rash」の実力を見てみましょう。

Grid Rashはユーロドルに最適化されたEAです。

始値のみでスプレッド0.2pipsでバックテストをすると、このように右肩上がりの資産チャートになります。


始値のみでスプレッド0.2pipsでのバックテスト

2013年から2024年9月までのユーロドルのプロフィットファクターは1.10です。

ちなみに、TDSによる変動スプレッドでの全ティックバックテストではプロフィットファクターは1.04になります。


TDSによる変動スプレッドでの全ティックバックテスト

ところで、ドル円では優位性はあるのでしょうか?

ドル円でのバックテストは1.00と優位性は無いようです。

それでは、売買フィルターを有効にして、MT4の最適化機能を使いバックテストをしてみます。

最適化機能の使い方ですが、このようにプロパティー設定画面のスタートの値とステップとストップの値を指定することで利用できます。

例えば、短期移動平均の値をスタートが10にして、ステップの値つまり刻みの数字を10とします。

ストップは、最大値みたいな感じですね。

ストップを50で設定した場合、10からスタートして20、30、40、50と順番にバックテストします。

バックテストの結果は一覧で表示されるので、一度パラメーターを設定すれば何百通りのバックテストを一度に行える機能になります。

実際の使い方として、短期移動平均だけでなく、長期移動平均も同時に設定します。

長期移動平均の値をスタートが50でステップが20でストップが250とします。

短期移動平均の組み合わせが5通りで長期移動平均の組み合わせが11通りなので、5かける11で55通りの組み合わせを一度に行うことが出来るのです。

こうすることで、膨大にある組み合わせの中から、最適な設定を探すことができるので、最適化機能という名前が付いているのです。

ちなみに、ボリンジャーバンドのシグマの場合、スタートが1でステップが1でストップが3だと、ボリンジャーバンドの値は1、2、3の順番でテストされます。

RSIの場合、RSIの下限のスタートが10でステップが10でストップが40だと、10、20、30、40の順番でテストされます。
同時にRSIの上限も設定するので、上限のスタートが60でステップが10でストップが90だと、60、70、80、90の順番でテストされます。

プロパティー画面の左のチェックボックスにチェックを入れると、その項目に対しスタートからストップの値までの順番にテストすることになるので、項目を増やし過ぎると何千通りものバックテストになってしまい、時間がかかります。

なので、項目を分けたり、ステップの値を大きくしてバックテストの回数をあまり増やさないようにしましょう。

多くても数百通りで抑えましょう。

ちなみに、全ティックでバックテストするととても時間がかかるので、最適化を使用する場合は始値のみの簡易バックテストで良さそうな値を調べます。

ある程度絞られたら、全ティックでバックテストすると良いでしょう。

それでは順番に最適化機能を使いバックテストしてみます。

有効にするフィルターの番号をプロパティー項目で選択します。

1は移動平均、2はボリンジャーバンド、3はマックディー、 4はRSI、5はADXです。

バックテストで一番成績の良かった結果をExcelシート上にプロフィットファクターだけを記録していきます。

バックテスト結果

フィルターなしの時のプロフィットファクターは1.10です。

始値のみでスプレッド0.2pipsでバックテストします。

移動平均線の最適な値のプロフィットファクターは1.12でした。

次に、ボリンジャーバンドのフィルターを試してみます。

ボリンジャーバンドの最適な値のプロフィットファクターは1.11でした。

マックディーはダメですね。一番良い成績でも、プロフィットファクターは1.09でした。

ちなみに、短期と長期が同じ期間だと、フィルターが機能しないので、フィルターなしと同じ結果になります。

次にRSIです。

RSIは良さそうです。

RSIの期間が9だとこのように成績が悪くなります。

反対に、期間が59だと上昇する傾向にあることから、期間を59以上で再度バックテストしてみようと思います。

一番良い成績のプロフィットファクターは1.11でした。

最後にADXです。

ADXは設定によっては極端にトレード回数が減りますね。

トレード回数とプロフィットファクターのバランスを考えて、一番よさそうな設定でのプロフィットファクターは1.16でした。

ADXの最適な設定は、Periodは40で、Thresholdは15でした。


ADXの最適な設定、Period40、Threshold15

全体的に見て、ADXだけ飛びぬけて好成績になったので、全ティックでバックテストしてみたいと思います。


全ティックでバックテスト(ADX)

わたしのEA開発の経験上、高頻度トレードのEAはプロフィットファクターは下がる傾向にあります。

エントリー回数が多いとスプレッドコストが負担となり、成績が悪化するからです。

Grid Rashはユーロドルということで、TDSの変動スプレッドだと、おそらく平均スプレッドは0.5pips程度になると思います。

試しに固定スプレッドで全ティックでバックテストします。


0.5pips固定 全ティックでバックテスト(ADX)

変動スプレッドと0.5pips固定だとほとんど同じ成績になりました。

つまり、MT4だとスプレッドの狭い国内FX会社でしか、この数値は実現できないと思います。

裁量トレーダー向けに、ロジックはそのままにしてサインインジケーターを作るのも面白いかもしれませんね。

変動スプレッドでは、驚異のプロフィットファクター1.10です。

トレード回数は20756回とフィルターなしの33649回と比べて半分近く減りましたが、安定性は格段に上がりました。

わたしがこれまで無料で配布してきたEAの中でも最高のパフォーマンスとなりました。

直近では横ばいの期間があることから、今後も同じ成績が出せるかはわかりませんが、無料のEAとしては最高クラスのパフォーマンスだと思います。
EAのソースコードは下記よりダウンロードできます。


//---START--------
#property copyright "Copyright 2024, Masayan."
#property version   "1.01"
#property strict
#property description "https://fx.reform-network.net"

extern int Magic = 20240615;  // Magic number
extern double Lots = 0.1;     // ロットサイズ
extern double StopLossRequest = 4.0;   // ストップロス幅
extern double TakeProfitRequest = 5.0; // テイクプロフィット幅
extern int MaxSpread = 50;     // 最大スプレッド
extern int MaxError = 100;     // 最大エラーカウント
extern double GridSize = 0.05;  // カギ足のサイズ(最小値0.01=1pips 推奨値0.05=5pips)
extern string CommentPositions = "Grid Rash Ver3";

extern int FilterType = 5; // 0:フィルターなし, 1:移動平均, 2:ボリンジャーバンド, 3:MACD, 4:RSI, 5:ADX
extern int ShortMAPeriod = 50;
extern int LongMAPeriod = 150;
extern int BBPeriod = 60;
extern double BBDeviation = 2.0;
extern int MACDFastPeriod = 26;
extern int MACDSlowPeriod = 27;
extern int MACDSignalPeriod = 9;
extern int RSIPeriod = 70;
extern double RSIUpper = 60.0;
extern double RSILower = 40.0;
extern int ADXPeriod = 40;
extern double ADXThreshold = 15.0;

//ここから改変可能
double lastBrickHigh = 0;  // 前回のカギ足高値
double lastBrickLow = 0;   // 前回のカギ足安値
//ここまで改変可能

string tmpstr;
string error_msg;
string spread_msg;
string position_msg;
bool LongSign = false;
bool ShortSign = false;
double Pips = 0.01;
int e = 0;
int Adjusted_Slippage = 0;

void OnTick()
{
    if (Bars < 10) return;

    double currentPrice = iClose(NULL, 0, 0);
    double Info_Spread = MarketInfo(Symbol(), MODE_SPREAD);
string value = Symbol();
string target = "JPY";
int pos = StringFind(value, target);
double Symbol_RATE = Close[1];
if(pos > 0){
Pips = 1.00;// ドルストレートは0.010(100pips)、クロス円は1.0(100pips)で判定
}else if(Symbol_RATE > 10 && Symbol_RATE <= 100){
Pips = 0.1;
}else if(Symbol_RATE > 100 && Symbol_RATE <= 1000){
Pips = 1.0;
}else if(Symbol_RATE > 1000 && Symbol_RATE <= 10000){
Pips = 10.0;
}else if(Symbol_RATE > 10000 && Symbol_RATE <= 100000){
Pips = 100.0;
}else if(Symbol_RATE > 100000){
Pips = 1000.0;
}

bool FilterCondition = true;  // フィルター条件の初期値をtrueに設定

switch (FilterType) {
    case 1: // 移動平均線フィルター
    {
        double shortMA = iMA(NULL, 0, ShortMAPeriod, 0, MODE_SMA, PRICE_CLOSE, 1);
        double longMA = iMA(NULL, 0, LongMAPeriod, 0, MODE_SMA, PRICE_CLOSE, 1);
        if (shortMA > longMA) {
            FilterCondition = true; // 上昇トレンド
        } else if (shortMA < longMA) {
            FilterCondition = false; // 下降トレンド
        }
        break;
    }
    case 2: // ボリンジャーバンドフィルター
    {
        double upperBand = iBands(NULL, 0, BBPeriod, BBDeviation, 0, PRICE_CLOSE, MODE_UPPER, 1);
        double lowerBand = iBands(NULL, 0, BBPeriod, BBDeviation, 0, PRICE_CLOSE, MODE_LOWER, 1);
        if (currentPrice > upperBand) {
            FilterCondition = false; // 高値圏
        } else if (currentPrice < lowerBand) {
            FilterCondition = true; // 安値圏
        }
        break;
    }
    case 3: // MACDフィルター
    {
        double macd = iMACD(NULL, 0, MACDFastPeriod, MACDSlowPeriod, MACDSignalPeriod, PRICE_CLOSE, MODE_MAIN, 1);
        double signal = iMACD(NULL, 0, MACDFastPeriod, MACDSlowPeriod, MACDSignalPeriod, PRICE_CLOSE, MODE_SIGNAL, 1);
        if (macd > signal) {
            FilterCondition = true; // 上昇トレンド
        } else if (macd < signal) {
            FilterCondition = false; // 下降トレンド
        }
        break;
    }
    case 4: // RSIフィルター
    {
        double rsi = iRSI(NULL, 0, RSIPeriod, PRICE_CLOSE, 1);
        if (rsi > RSIUpper) {
            FilterCondition = false; // 過熱圏
        } else if (rsi < RSILower) {
            FilterCondition = true; // 買いシグナル
        }
        break;
    }
    case 5: // ADXフィルター
    {
        double adx = iADX(NULL, 0, ADXPeriod, PRICE_CLOSE, MODE_MAIN, 1);
        if (adx > ADXThreshold) {
            FilterCondition = true; // 強いトレンド
        } else {
            FilterCondition = false; // トレンドが弱い
        }
        break;
    }
    default: // フィルターなし
        FilterCondition = true;
        break;
}

// カギ足ロジックを生成する

    // 初回は基準の高値・安値を設定
    if (lastBrickHigh == 0 && lastBrickLow == 0) {
        lastBrickHigh = currentPrice;
        lastBrickLow = currentPrice;
        return;
    }

    // カギ足の生成条件チェック
    if (currentPrice >= lastBrickHigh + (GridSize * Pips) && Seconds() < 3 && FilterCondition) { 
        ShortSign = true;
        LongSign = false;
        lastBrickHigh = currentPrice;
        lastBrickLow = currentPrice - (GridSize * Pips);
    } else if (currentPrice <= lastBrickLow - (GridSize * Pips) && Seconds() < 3 && FilterCondition) {
        LongSign = true;
        ShortSign = false;
        lastBrickLow = currentPrice;
        lastBrickHigh = currentPrice + (GridSize * Pips);
    } else {
        LongSign = false;
        ShortSign = false;
    }


/*ここに決済ロジックを挿入*/



    // スプレッドのチェック
    if (MaxSpread < Info_Spread) {
        LongSign = false;
        ShortSign = false;
        spread_msg = "Max spread Orber\n";
    } else {
        spread_msg = "";
    }

    if (Hour() == 4 && Minute() == 0 && Seconds() < 6) e = 0;
    if (Hour() == 11 && Minute() == 0 && Seconds() < 6) e = 0;
    if (Hour() == 18 && Minute() == 0 && Seconds() < 6) e = 0;

    if (e > MaxError) {
        LongSign = false;
        ShortSign = false;
        error_msg = "Continuous order count limit\n";
    } else {
        error_msg = "";
    }

    if (LongSign) position_msg = "LongSign = true\n";
    else if (ShortSign) position_msg = "ShortSign = true\n";
    else position_msg = "No Sign\n";

    // 売買指示
    if (LongSign) {
        if (CalculateCurrentOrders() == 0) {
            CheckForOpenLong();
            e++;
        } else {
            CloseShortPosition();
        }
    }

    if (ShortSign) {
        if (CalculateCurrentOrders() == 0) {
            CheckForOpenShort();
            e++;
        } else {
            CloseLongPosition();
        }
    }

    tmpstr = StringConcatenate(error_msg, spread_msg, position_msg);
    Comment(tmpstr);
}




// 保有中のポジションを計算
int CalculateCurrentOrders(void)
{
    int buys = 0;
    int sells = 0;
    for (int icount = 0; icount < OrdersTotal(); icount++) {
        if (!OrderSelect(icount, SELECT_BY_POS, MODE_TRADES)) break;
        if (OrderSymbol() == Symbol() && OrderMagicNumber() == Magic) {
            if (OrderType() == OP_BUY) buys++;
            if (OrderType() == OP_SELL) sells++;
        }
    }
    return (buys > 0) ? buys : -sells;
}

//+------------------------------------------------------------------+
//| ロングオーダー処理                                               |
//+------------------------------------------------------------------+
void CheckForOpenLong() {
    int    res;      // オーダー送信戻り値用
    double entrylot; // オーダー用ロット
    entrylot = NormalizeDouble(Lots,2); // 入力パラメータのロット数を、小数点第二位で正規化
//■□■□■□■□■ ロングエントリー ■□■□■□■□■
        res = OrderSend(
                        Symbol(),       // 現在の通貨ペア
                        OP_BUY,         // ロングエントリー(成行注文)
                        entrylot,        // ロット設定
                        Ask,            // 現在の買値で発注
                        Adjusted_Slippage,// スリップページ
                        Ask - (Pips * StopLossRequest),              // ストップロス設定
                        Ask + (Pips * TakeProfitRequest),              // リミット設定
                        CommentPositions,       // ロングコメント
                        Magic,        // マジックナンバー
                        0,              // 有効期限:無し
                        clrRed);          // チャート上の注文矢印の色:赤
        return; // 関数処理終了
}

//+------------------------------------------------------------------+
//| ショートオーダー処理                                             |
//+------------------------------------------------------------------+
void CheckForOpenShort() {
    int    res;      // オーダー送信戻り値用
    double entrylot; // オーダー用ロット
    entrylot = NormalizeDouble(Lots,2); // 入力パラメータのロット数を、小数点第二位で正規化
//■□■□■□■□■ ショートエントリー ■□■□■□■□■
        res = OrderSend(
                        Symbol(),        // 現在の通貨ペア
                        OP_SELL,         // ショートエントリー(成行注文)
                        entrylot,        // ロット設定
                        Bid,             // 現在の売値で発注
                        Adjusted_Slippage,// スリップページ
                        Bid + (Pips * StopLossRequest),               // ストップロス設定
                        Bid - (Pips * TakeProfitRequest),               // リミット設定
                        CommentPositions,      // ショートコメント
                        Magic,         // マジックナンバー
                        0,               // 有効期限:無し
                        clrBlue);            // チャート上の注文矢印の色:青
        return; // 関数処理終了
}

// ショートポジションをクローズ
void CloseShortPosition()
{
    for (int icount = 0; icount < OrdersTotal(); icount++) {
        if (!OrderSelect(icount, SELECT_BY_POS, MODE_TRADES)) break;
        if (OrderMagicNumber() != Magic || OrderSymbol() != Symbol()) continue;
        if (OrderType() == OP_SELL) {
            if (!OrderClose(OrderTicket(), OrderLots(), Ask, Adjusted_Slippage, clrBlue)) 
                Print("エラーコード=", GetLastError());
            break;
        }
    }
}

// ロングポジションをクローズ
void CloseLongPosition()
{
    for (int icount = 0; icount < OrdersTotal(); icount++) {
        if (!OrderSelect(icount, SELECT_BY_POS, MODE_TRADES)) break;
        if (OrderMagicNumber() != Magic || OrderSymbol() != Symbol()) continue;
        if (OrderType() == OP_BUY) {
            if (!OrderClose(OrderTicket(), OrderLots(), Bid, Adjusted_Slippage, clrRed)) 
                Print("エラーコード=", GetLastError());
            break;
        }
    }
}
//---END---

EA開発に自信のある方は、ロジックのところを変更するなどしてカスタマイズしてみてください。

好成績のEAが完成した際には、コメント頂けると幸いです。

次回の動画ではGrid Rashに、ローソク足によるプライスアクション手法をロジックに組み込んでみたいと思います。

その後の作業として、イグジットつまり決済のロジックを追加する予定です。

いずれの作業もEA開発未経験の人には難しい作業となりますが、ChatGPTをフル活用して付いてきていただければと思います。

他にも、こんな手法EAにしてほしいみたいなリクエストがあれば、コメントお願いします。

【免責事項】
・本GPTsについて、正当性を保証するものではありません。
・本GPTsを利用して損失を被った場合でも一切の責任を負いません。
・投資の決定は、自己判断 自己責任でお願いします。

この記事が気に入ったらサポートをしてみませんか?