ネットで見つけた複利トレードEAのサンプルコードをそのまま使うと、パラメーターに「StopLoss」という項目があるのに実際の注文には反映されていなかったり、損切り幅と関係なく証拠金の額だけでロット数を決めてしまい、思ったより大きな損失リスクを抱えていたりすることがあります。
この記事では、口座の証拠金に応じてロット数を自動で増減させる複利トレードEAを例に、パラメーターの設定からOnTick()関数の中身、そして見落としがちな「ポジションの重複防止」「損切りを実際に効かせる」「損切り幅を考慮したロット計算」まで、実際にMetaEditorでコンパイルしてMT4で動かせる状態のコードを最後まで解説します。
複利でトレードするEA
複利とは、トレードによって証拠金が増えるほど、それに応じてEAの稼働ロットも増やしていくスタイルです。
- 単利:証拠金に応じたロット数
- 複利:証拠金+利益に応じたロット数
複利トレードは、口座の資金が取引により増えるたびに、ロット数を増やしていきます。
逆に資金が減って行くとロット数も減って行きます。
今回は移動平均線のゴールデンクロスとデッドクロスのロジックを活用した
複利でトレードするEAを作成してきたいと思います。
メタエディタ(MetaEditor)を立ち上げる
MT4を起動し、ツールバーの黄色いボタン(またはツール→MetaQuotes Language Editor)からメタエディタ(MetaEditor)を立ち上げましょう。
新しいEAを作るので、メタエディタの「ファイル」→「新規作成」を選び、ウィザードで「Expert Advisor(テンプレートを使用)」を選択して「次へ」を押します。
名前を入力する画面が出るので、今回は「Fukuri-EA」と入力します(作者名・リンク・バージョンは空欄のままで問題ありません)。あとの画面はイベントハンドラのチェックを何も入れず、そのまま「次へ」を数回押して「完了」まで進めてください。すると、OnInit()・OnDeinit()・OnTick()の3つの関数だけが最初から書かれた空のひな形が開きます。
このひな形に、これから説明するコードを追記していきます。
パラメーターを記述する
パラメーターを記述しましょう。
ロットをユーザーが設定するための変数Lotsを用意します。
input double Lots = 0.01; //ロット数
次に複利機能をON/OFFするための変数をbool型の変数を用意します。
input bool MM = false; //複利モードON/OFF
証拠金の%に応じて複利をするためのRisk変数を用意します。
input double Risk = 1.0; //1%の証拠金を対象に
あとはいつものようにトレードに必要な機能を用意しておきましょう。損切り(StopLoss)とスリッページ(Slippage)は、後述する理由でこの記事から新しく追加したパラメーターです。
input int StopLoss = 100;//ストップロス input int TakeProfit = 5;//利確 input int MagicNumber = 12345; //マジックナンバー input int Slippage = 3;//スリッページ input int MA_Short_period = 50;//MA期間 input int MA_Long_period = 100;//MA期間
パラメーター全体のソースコードは以下の記載しています。
input double Lots = 0.01; //ロット数 input bool MM = false; //複利モードON/OFF input double Risk = 1.0; //1%の証拠金を対象に input int StopLoss = 100;//ストップロス input int TakeProfit = 5;//利確 input int MagicNumber = 12345; //マジックナンバー input int Slippage = 3;//スリッページ input int MA_Short_period = 50;//MA期間 input int MA_Long_period = 100;//MA期間
OnTick()関数を記述する
OnTick()関数を記述しましょう。
まずは、移動平均線の過去と現在の変数を短期と長期両方用意します。
double kakoMAS,kakoMAL;//過去の短期MAと長期MA double genMAS,genMAL;//現在の短期MAと長期MA
過去のMAはシフトを1にしましょう。
//過去の短期MA kakoMAS = iMA(NULL,0,MA_Short_period,0,MODE_SMA,PRICE_CLOSE,1); //過去の長期MA kakoMAL = iMA(NULL,0,MA_Long_period,0,MODE_SMA,PRICE_CLOSE,1);
現在の足は0にしましょう。
//現在の短期MA genMAS = iMA(NULL,0,MA_Short_period,0,MODE_SMA,PRICE_CLOSE,0); //現在の長期MA genMAL= iMA(NULL,0,MA_Long_period,0,MODE_SMA,PRICE_CLOSE,0);
MMがtrueの時に複利機能をONにします。
そしてゴールデンクロス時とデッドクロス時にOrderSend関数でポジションを建てます。
その際にLotにLotSize()というユーザー定義関数を用意しました。
これが複利を計算する関数となります。
if(MM==true)//複利がONの場合
{
//ゴールデンクロス時
if(kakoMAS <= kakoMAL && genMAS > genMAL)
{
OrderSend(Symbol(),OP_BUY,LotSize(),Ask,Slippage,Ask-StopLoss*Point*10,Ask+TakeProfit*Point*10,"Order Long",MagicNumber,0,Red);
}
//デッドクロス時
if( kakoMAS >= kakoMAL && genMAS < genMAL)
{
OrderSend(Symbol(),OP_SELL,LotSize(),Bid,Slippage,Bid+StopLoss*Point*10,Bid-TakeProfit*Point*10,"Order Short",MagicNumber,0,Blue);
}
}
複利機能がOFFの場合は通常通りにゴールデンクロスとデッドクロスはLotsでimputのLotsのサイズでトレードするようになっています。
else//複利がOFFの場合
{
//ゴールデンクロス時
if(kakoMAS <= kakoMAL && genMAS > genMAL)
{
OrderSend(Symbol(),OP_BUY,Lots,Ask,Slippage,Ask-StopLoss*Point*10,Ask+TakeProfit*Point*10,"Order Long",MagicNumber,0,Red);
}
//デッドクロス時
if( kakoMAS >= kakoMAL && genMAS < genMAL)
{
OrderSend(Symbol(),OP_SELL,Lots,Bid,Slippage,Bid-TakeProfit*Point*10,Bid-TakeProfit*Point*10,"Order Short",MagicNumber,0,Blue);
}
}
このロジック自体はエントリーの判定として正しいのですが、このまま動かすとまだ3つの問題が残っています。次のセクションで直していきます。
(元のコードには「もし50日移動平均線が100日線を下から上にクロスしたら」の条件が `kakoMAS <+ kakoMAL` と書かれ、`<=`(以下)のつもりで打った `+` が紛れ込むタイポがありました。`<+` はMQLの演算子としては存在せず、単項プラスとして解釈されるため実質的に動作はしますが、意図と違う書き方になっていたため `<=` に修正しています。)
ポジションを重複して持たないようにする
上のコードのままOnTick()を動かすと、ゴールデンクロス・デッドクロスの条件が成立している間(次に反対方向へクロスするまでの間、数時間〜数日続くこともあります)、毎ティック新規のエントリーを送り続けてしまいます。
複利モードでは証拠金に応じてロット数も変わるため、放置すると意図しないロットのポジションが際限なく積み上がる結果になります。
これを防ぐため、まず自分(このマジックナンバー)のポジションをすでに持っているかどうかを判定する関数を用意し、OnTick()の先頭でチェックします。
bool HasPosition()
{
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);
}
OrdersTotal()で保有中の注文数だけループし、通貨ペアとマジックナンバーが一致するポジションが1つでもあればtrueを返します。OnTick()の先頭に以下の1行を追加するだけで、ポジションを持っている間は新規エントリーの判定自体をスキップできます。
if(HasPosition()) return;//既にポジションがあれば新規エントリーしない
損切り(ストップロス)を実際に効かせる
パラメーターには`StopLoss`という項目が用意されていましたが、元のOrderSend()は6番目の引数(損切り価格)が常に`0`になっており、パラメーターで設定した値がまったく使われていませんでした。0を指定すると損切りは設定されず、デッドクロス(またはゴールデンクロス)が起きてポジションが逆行しても決済されないまま含み損が膨らみ続けます。
StopLossを使って、エントリー方向と逆側に損切りラインを計算するよう修正します。
//買いの場合 Ask-StopLoss*Point*10 //売りの場合 Bid+StopLoss*Point*10
※Pointは通貨ペアの最小価格単位です。多くのFX業者が採用している5桁表示(例:USDJPYが150.123のように小数点以下3桁)のブローカーでは1pip=10Pointになるため、pips単位で指定したTakeProfit・StopLossに10を掛けています。4桁表示(USDJPYが150.12までの2桁)のブローカーを使う場合は、この「×10」を外してください。
※このEAをそのまま動かして「エラーコード130(Invalid stops)」がログに出た場合、注文が失敗する原因のほとんどはStopLoss・TakeProfitの値が近すぎることです。ブローカーごとに決まっている最小ストップ距離(ストップレベル)より値が小さい場合に発生するため、エラー130が出たらStopLoss・TakeProfitの値を大きくしてみてください。
LotSize()を記述する
複利モード(MM=true)の時に使うロット計算関数LotSize()を用意します。
元の考え方は「有効証拠金 × リスク% に応じてロットを決める」というものでしたが、これだけだとStopLoss(損切り幅)が考慮されず、StopLossを100pipsにしても10pipsにしても同じロット数になってしまいます。同じ「Risk=1.0(1%)」という設定でも、損切り幅が広ければ広いほど実際の損失額は大きくなるため、これでは「Risk」という名前と実際の損失リスクが一致しません。
そこで、StopLoss(pips)とその通貨ペアの1pipあたりの価値をもとに、「損切りに達したときの損失額が、口座残高のRisk%にちょうど収まるロット数」を逆算する計算式に直します。
double tickValue = MarketInfo(Symbol(),MODE_TICKVALUE);//1ティックあたりの損益(口座通貨換算) double tickSize = MarketInfo(Symbol(),MODE_TICKSIZE); double pipValue = tickValue * (Point*10/tickSize);//1pipあたりの損益 double riskAmount = AccountBalance() * Risk / 100.0;//許容する損失額 double lot = riskAmount / (StopLoss * pipValue);
MarketInfo(Symbol(),MODE_TICKVALUE)は、その通貨ペアを1ロット保有したときの1ティックあたりの損益を口座通貨で返す関数です。これとStopLossの値を掛け合わせることで、「このロット数・このStopLoss幅でエントリーしたときに、損切りに達すると何円(何ドル)の損失になるか」を計算し、それがRisk%ちょうどになるようロットを逆算しています。
例えば、口座残高が50万円、Risk=1.0(1%)、StopLoss=100pips、USDJPYの1pipあたりの価値が1ロットで約1,000円だとすると、許容する損失額(riskAmount)は50万円×1%=5,000円。5,000円 ÷(100pips×1,000円)=0.05ロットが、この設定でエントリーするロット数になります。StopLossを200pipsに広げれば、同じRisk1%でもロットは半分の0.025ロットに自動で調整されます。
計算したロットは、そのままだとブローカーが許可する最小ロット・最大ロット・ロットの刻み幅(ロットステップ)に合っていない可能性があるため、丸め処理を入れます。
double minLot = MarketInfo(Symbol(),MODE_MINLOT); double maxLot = MarketInfo(Symbol(),MODE_MAXLOT); double lotStep = MarketInfo(Symbol(),MODE_LOTSTEP); lot = MathFloor(lot/lotStep)*lotStep;//ロットステップに合わせて切り捨て if(lot < minLot) lot = minLot; if(lot > maxLot) lot = maxLot;
最後にreturnでlotを戻り値として入れたら完了です。
return (lot);
LotSize()の全体のソースコードは以下に記載しています。
double LotSize()
{
double tickValue = MarketInfo(Symbol(),MODE_TICKVALUE);//1ティックあたりの損益(口座通貨換算)
double tickSize = MarketInfo(Symbol(),MODE_TICKSIZE);
double pipValue = tickValue * (Point*10/tickSize);//1pipあたりの損益
double riskAmount = AccountBalance() * Risk / 100.0;//許容する損失額
double lot = riskAmount / (StopLoss * pipValue);
double minLot = MarketInfo(Symbol(),MODE_MINLOT);
double maxLot = MarketInfo(Symbol(),MODE_MAXLOT);
double lotStep = MarketInfo(Symbol(),MODE_LOTSTEP);
lot = MathFloor(lot/lotStep)*lotStep;//ロットステップに合わせて切り捨て
if(lot < minLot) lot = minLot;
if(lot > maxLot) lot = maxLot;
return(lot);
}
コンパイル
コンパイルして稼働して複利でロットが増えていたら完了です。
ソースコード全体
Fukuri-EAの全体を反映した、実際にMetaEditorでコンパイルしてそのまま動かせる状態のコードです。
//+------------------------------------------------------------------+
//| Fukuri-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; //ロット数(複利モードOFF時)
input bool MM = false;//複利モードON/OFF
input double Risk = 1.0; //複利モード時のリスク(%)
input int StopLoss = 100; //ストップロス(pips)
input int TakeProfit = 5; //利確(pips)
input int MagicNumber = 12345;//マジックナンバー
input int Slippage = 3; //スリッページ
input int MA_Short_period = 50; //MA期間(短期)
input int MA_Long_period = 100; //MA期間(長期)
//+------------------------------------------------------------------+
//| このEA(マジックナンバー)のポジションを保有しているか判定する |
//+------------------------------------------------------------------+
bool HasPosition()
{
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);
}
//+------------------------------------------------------------------+
//| StopLoss幅から、Risk(%)に収まるロット数を計算する |
//+------------------------------------------------------------------+
double LotSize()
{
double tickValue = MarketInfo(Symbol(),MODE_TICKVALUE);//1ティックあたりの損益(口座通貨換算)
double tickSize = MarketInfo(Symbol(),MODE_TICKSIZE);
double pipValue = tickValue * (Point*10/tickSize);//1pipあたりの損益
double riskAmount = AccountBalance() * Risk / 100.0;//許容する損失額
double lot = riskAmount / (StopLoss * pipValue);
double minLot = MarketInfo(Symbol(),MODE_MINLOT);
double maxLot = MarketInfo(Symbol(),MODE_MAXLOT);
double lotStep = MarketInfo(Symbol(),MODE_LOTSTEP);
lot = MathFloor(lot/lotStep)*lotStep;//ロットステップに合わせて切り捨て
if(lot < minLot) lot = minLot;
if(lot > maxLot) lot = maxLot;
return(lot);
}
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
//---
//---
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
//---
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
//---
//既にポジションを持っている場合は新規エントリーしない
if(HasPosition())
return;
double kakoMAS,kakoMAL;//過去の短期MAと長期MA
double genMAS,genMAL;//現在の短期MAと長期MA
//過去の短期MA
kakoMAS = iMA(NULL,0,MA_Short_period,0,MODE_SMA,PRICE_CLOSE,1);
//過去の長期MA
kakoMAL = iMA(NULL,0,MA_Long_period,0,MODE_SMA,PRICE_CLOSE,1);
//現在の短期MA
genMAS = iMA(NULL,0,MA_Short_period,0,MODE_SMA,PRICE_CLOSE,0);
//現在の長期MA
genMAL= iMA(NULL,0,MA_Long_period,0,MODE_SMA,PRICE_CLOSE,0);
double lot = MM ? LotSize() : Lots;//複利がONならLotSize()、OFFならLots
int ticket;
//ゴールデンクロス時
if(kakoMAS <= kakoMAL && genMAS > genMAL)
{
ticket = OrderSend(Symbol(),OP_BUY,lot,Ask,Slippage,Ask-StopLoss*Point*10,Ask+TakeProfit*Point*10,"Order Long",MagicNumber,0,Red);
if(ticket < 0)
Print("買い注文に失敗しました。エラーコード=",GetLastError());
}
//デッドクロス時
if(kakoMAS >= kakoMAL && genMAS < genMAL)
{
ticket = OrderSend(Symbol(),OP_SELL,lot,Bid,Slippage,Bid+StopLoss*Point*10,Bid-TakeProfit*Point*10,"Order Short",MagicNumber,0,Blue);
if(ticket < 0)
Print("売り注文に失敗しました。エラーコード=",GetLastError());
}
}
//+------------------------------------------------------------------+
バックテストで動作を確認する
コードを保存したら、MetaEditorの「コンパイル」(Ctrl+F7)で「0 error(s), 0 warning(s)」になることを確認します。
そのあとMT4の「表示」→「ストラテジーテスター」を開き、Expert AdvisorにFukuri-EAを選択、MMをtrueにして通貨ペアと期間を指定し、モデルを「全ティック(最も正確な値)」にしてスタートすると、証拠金が増減するのに応じてロット数が変わる様子を確認できます。
実運用する前に確認しておきたいこと
このEAは移動平均線のクロスだけで売買する、学習用にシンプルへ作った例です。実際の口座で稼働させる前に、次の点を確認してください。
- 複利モード(Risk%ベースのロット計算)は、連敗が続いて証拠金が減ると自動的にロットも小さくなりますが、逆に連勝すると自動的にロットが大きくなるため、想定より早いペースでリスクが拡大することがある
- レンジ相場ではゴールデンクロス・デッドクロスが短時間で連続して発生しやすく、損切りが重なりやすい(いわゆる「ダマシ」)
- バックテストの結果は過去データに基づくものであり、将来の相場でも同じ成績になるとは限らない
まずはデモ口座、または少額のロットでのフォワードテスト(リアルタイムでの動作確認)を行ってから、本番運用を検討してください。
まとめ
今回は複利でトレードするEAについてでした。エントリーのロジックだけでなく、ポジションの重複防止・損切りを実際に効かせる設定・損切り幅を考慮したロット計算まで含めて初めて、実運用を検討できる形になります。
コメント