ネットにあるナンピンEAのサンプルコードは、一見すると動きそうに見えても、if文の書き方ひとつで「意図しない条件で注文を出し続ける」誤作動を起こすことがあります。特にナンピン系のEAは、正しく組まないと証拠金が尽きるまでポジションを増やし続けるリスクがあるため、コードの一字一句を丁寧に確認する必要があります。
この記事では、RSIが売られすぎ・買われすぎのタイミングで逆張りエントリーし、含み損が一定幅を超えたらナンピン(追加エントリー)するEAを例に、パラメーターの設定からポジション管理、OnTick()関数の中身まで、実際にMetaEditorでコンパイルしてMT4で動かせる状態のコードを解説します。
ナンピンでトレードするEA
ナンピンでトレードするEAを作成していきたいと思います。
- RSIが70以上か30以下の場合に逆張りトレード
- チャートが逆行した場合にナンピンする
メタエディタ(MetaEditor)を立ち上げる
MT4を起動し、ツールバーの黄色いボタン(またはツール→MetaQuotes Language Editor)からメタエディタ(MetaEditor)を立ち上げましょう。今回は名前を「Nampin-EA」で作成します。
メタエディタの「ファイル」→「新規作成」を選び、ウィザードで「Expert Advisor(テンプレートを使用)」を選択して名前を入力し、そのまま「次へ」を数回押して「完了」まで進めてください。OnInit()・OnDeinit()・OnTick()の3つの関数だけが書かれた空のひな形が開きます。
パラメーターを記述する
パラメーターを記述しましょう。
input double Lots = 0.01;//初回ロット数 input int Slippage = 3;//スリッページ input int RSI_period = 14;//RSIの期間 input int NampinPips = 50;//ナンピンする値幅(pips) input int MaxNampin = 5;//最大ナンピン数 input int MagicNumber = 12345;//マジックナンバー input int StopLoss = 100;//損切り(pips) input int TakeProfit = 5;//利確(pips)
NampinPipsは、最後に建てたポジションの始値からどれだけ逆行したら次のナンピンを入れるかを決める値幅です。値が小さいほど頻繁にナンピンが発動し、保有ポジション数がすぐに増えます。まずはこの値のままバックテストし、通貨ペアやそのときのボラティリティに合わせて調整してください。
※このEAを動かして「エラーコード130(Invalid stops)」がログに出た場合、原因の多くはStopLoss・TakeProfitの値がブローカーの最小ストップ距離より近いことです。エラー130が出た場合は、まずStopLoss・TakeProfitの値を大きくしてみてください。
グローバル変数を記述する
グローバル変数を記述しましょう。
static int TicketNumber;//チケットナンバー
自分のポジション数を数える関数を用意する
元のコードは、ポジション数を`OrdersTotal()`(口座全体の保有注文数)でそのまま判定していました。これだと、もし同じ口座で他のEAや裁量トレードのポジションを持っていた場合、それらも「このEAのナンピン数」として数えてしまい、最大ナンピン数の判定がずれてしまいます。
このEA(マジックナンバー)自身のポジションだけを数える関数と、その中で最後に見つかったポジションを選択する関数を用意します。
int CountMyPositions()
{
int count=0;
for(int i=OrdersTotal()-1; i>=0; i--)
{
if(!OrderSelect(i,SELECT_BY_POS,MODE_TRADES)) continue;
if(OrderSymbol()==Symbol() && OrderMagicNumber()==MagicNumber)
count++;
}
return(count);
}
bool SelectLastMyPosition()
{
for(int i=OrdersTotal()-1; i>=0; i--)
{
if(!OrderSelect(i,SELECT_BY_POS,MODE_TRADES)) continue;
if(OrderSymbol()==Symbol() && OrderMagicNumber()==MagicNumber)
return(true);
}
return(false);
}
OnTick()関数を記述する
OnTick()関数の中身を記述しましょう。まずポジション数を確認し、ポジションを持っていなければ新規エントリーの判定、すでに持っていればナンピンの判定を行います。
int posCount=CountMyPositions();
if(posCount==0)
{
//新規エントリーの判定
}
else
{
//ナンピンの判定
}
新規エントリーは、RSIが売られすぎ・買われすぎの水準を判定して行います。
double RSI=iRSI(Symbol(),0,RSI_period,PRICE_CLOSE,0);
if(RSI<30)//RSIが30を下回ったら逆張りロング
{
//ロングエントリー
}
else if(RSI>70)//RSIが70を上回ったら逆張りショート
{
//ショートエントリー
}
if文をコメントで壊さないよう注意する
実は、この記事のもとになったコードには、次のような書き方がありました。
if(RSI < 30) { ...買い注文... } //デッドクロス時 if(RSI > 70)
{
...売り注文...
}
一見「RSIが70を超えたら売る」という条件に見えますが、`if(RSI > 70)`の部分はコメント(`//`より後ろ)に含まれてしまっているため、コンパイラからは単なる中括弧`{ }`のブロックとして扱われます。つまり実際には、`if(RSI < 30){...}`という文がそこで完結したあと、条件なしの`{ }`ブロックが**RSIの値に関係なく毎回実行され**、常に売り注文が出てしまいます。 コンパイルはエラーなく通ってしまうため、見た目だけでは気づきにくいバグです。上のセクションのように、`if`と条件式・`else if`を省略せずきちんと書くことで、コメントに条件式が巻き込まれることを防げます。
すでにポジションを持っている場合のナンピン判定
すでにポジションを持っている場合は、最大ナンピン数に達していないか、そして価格が指定した値幅だけ逆行しているかを確認してから追加エントリーします。
if(posCount>=MaxNampin) return;//最大ナンピン数に達していたら何もしない
if(!SelectLastMyPosition()) return;
if(OrderType()==OP_BUY && Close[0]<(OrderOpenPrice()-NampinPips*Point*10))
{
//買い増し(ナンピン)
}
if(OrderType()==OP_SELL && Close[0]>(OrderOpenPrice()+NampinPips*Point*10))
{
//売り増し(ナンピン)
}
ソースコード全体
ここまでの修正をすべて反映した、実際にMetaEditorでコンパイルしてそのまま動かせる状態のコードです。
//+------------------------------------------------------------------+
//| Nampin-EA.mq4 |
//| Copyright 2020, FX-EA System Project Creator |
//| https://creator.fx-ea-system-project.com/ |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, FX-EA System Project Creator"
#property link "https://creator.fx-ea-system-project.com/"
#property version "1.01"
#property strict
input double Lots = 0.01;//初回ロット数
input int Slippage = 3;//スリッページ
input int RSI_period = 14;//RSIの期間
input int NampinPips = 50;//ナンピンする値幅(pips)
input int MaxNampin = 5;//最大ナンピン数
input int MagicNumber = 12345;//マジックナンバー
input int StopLoss = 100;//損切り(pips)
input int TakeProfit = 5;//利確(pips)
static int TicketNumber;//チケットナンバー
//+------------------------------------------------------------------+
//| このEA(マジックナンバー)のポジション数を数える |
//+------------------------------------------------------------------+
int CountMyPositions()
{
int count=0;
for(int i=OrdersTotal()-1; i>=0; i--)
{
if(!OrderSelect(i,SELECT_BY_POS,MODE_TRADES)) continue;
if(OrderSymbol()==Symbol() && OrderMagicNumber()==MagicNumber)
count++;
}
return(count);
}
//+------------------------------------------------------------------+
//| このEAが直近に建てたポジションを選択する |
//+------------------------------------------------------------------+
bool SelectLastMyPosition()
{
for(int i=OrdersTotal()-1; i>=0; i--)
{
if(!OrderSelect(i,SELECT_BY_POS,MODE_TRADES)) continue;
if(OrderSymbol()==Symbol() && OrderMagicNumber()==MagicNumber)
return(true);
}
return(false);
}
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
//---
//---
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
//---
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
//---
int posCount=CountMyPositions();
if(posCount==0)//ポジションを持っていない場合
{
double RSI=iRSI(Symbol(),0,RSI_period,PRICE_CLOSE,0);
if(RSI<30)//RSIが30を下回ったら逆張りロング
{
TicketNumber=OrderSend(Symbol(),OP_BUY,Lots,Ask,Slippage,Ask-StopLoss*Point*10,Ask+TakeProfit*Point*10,"buy",MagicNumber,0,Red);
if(TicketNumber<0)
Print("買い注文に失敗しました。エラーコード=",GetLastError());
}
else if(RSI>70)//RSIが70を上回ったら逆張りショート
{
TicketNumber=OrderSend(Symbol(),OP_SELL,Lots,Bid,Slippage,Bid+StopLoss*Point*10,Bid-TakeProfit*Point*10,"sell",MagicNumber,0,Blue);
if(TicketNumber<0)
Print("売り注文に失敗しました。エラーコード=",GetLastError());
}
}
else//すでにポジションを持っている場合(ナンピン判定)
{
if(posCount>=MaxNampin) return;//最大ナンピン数に達していたら何もしない
if(!SelectLastMyPosition()) return;
if(OrderType()==OP_BUY && Close[0]<(OrderOpenPrice()-NampinPips*Point*10))
{
TicketNumber=OrderSend(Symbol(),OP_BUY,OrderLots(),Ask,Slippage,Ask-StopLoss*Point*10,Ask+TakeProfit*Point*10,"buy2",MagicNumber,0,Blue);
if(TicketNumber<0)
Print("ナンピン買い注文に失敗しました。エラーコード=",GetLastError());
}
if(OrderType()==OP_SELL && Close[0]>(OrderOpenPrice()+NampinPips*Point*10))
{
TicketNumber=OrderSend(Symbol(),OP_SELL,OrderLots(),Bid,Slippage,Bid+StopLoss*Point*10,Bid-TakeProfit*Point*10,"sell2",MagicNumber,0,Red);
if(TicketNumber<0)
Print("ナンピン売り注文に失敗しました。エラーコード=",GetLastError());
}
}
}
//+------------------------------------------------------------------+
バックテストで動作を確認する
コードを保存したら、MetaEditorの「コンパイル」(Ctrl+F7)で「0 error(s), 0 warning(s)」になることを確認します。
そのあとMT4の「表示」→「ストラテジーテスター」を開き、Expert AdviserにNampin-EAを選択、通貨ペアと期間を指定して、モデルを「全ティック(最も正確な値)」にしてスタートすると、過去データでの動作を確認できます。
修正前のコードでバックテストすると、RSIの値に関わらず売り注文が繰り返し発生する様子が結果に表れます。修正後は、RSIが30を下回った時だけ買い、70を上回った時だけ売る動作になっていることを確認してください。
実運用する前に確認しておきたいこと
ナンピン手法は、含み損を抱えたまま追加エントリーするため、通常のEA以上に資金管理のリスクが高い手法です。実運用する前に、次の点を必ず確認してください。
- MaxNampinに到達すると、このEAは新規のナンピンを止めるだけで、既存ポジションを自動で決済する仕組みは持っていません。相場がそのまま戻らなければ、含み損を抱えたまま塩漬けになります。「保有中の全ポジションの合計損益が一定額を下回ったら強制決済する」処理を別途追加することを検討してください
- ナンピンのたびにロット数が同じ(等倍)ため、口座残高に対して必要な証拠金がナンピンのたびに増えていきます。MaxNampin×Lotsに必要な証拠金を口座残高から逆算しておいてください
- StopLoss(損切り)は各ポジションに個別に設定されていますが、ナンピンで複数ポジションを保有した状態でのトータルの損切りラインではありません
まずはデモ口座、または少額のロットでのフォワードテスト(リアルタイムでの動作確認)を行ってから、本番運用を検討してください。
まとめ
RSI逆張り×ナンピンでトレードするEAの作り方を、実際に動くコードとあわせて解説しました。if文の条件がコメントに巻き込まれるバグは見た目では気づきにくいため、コンパイルが通っても、実際の売買ロジックが意図通りかをバックテストで必ず確認する習慣をつけましょう。
コメント