このサイトのEA記事では「損切りを必ず入れる」ことを繰り返し扱ってきましたが、
含み益が出たあとに「どう決済するか」はまだ扱っていませんでした。
全部が一気に利確されるか、逆転して損切りに引っかかるかの2択だけでは、
伸びた利益を途中で一部だけ確保する、という選択肢がありません。
この記事では、OrderCloseのlots引数に全量より少ない数量を渡す「部分決済」
を使い、利益が一定pipsに達するたびにポジションを段階的に減らし、
同時にストップロスを建値へ移動していくEAを作ります。
・OrderCloseで注文の一部だけを決済する方法
・決済と同時にストップロスを建値へ移すOrderModifyの使い方
・同じチケットに2段階の決済を重複実行させない管理方法
・実際に動くコード全体
設計思想: なぜ決済管理だけを独立させるのか
まず、この記事のエントリー条件を意図的に簡素にしている理由を説明します。
このサイトには既にクロス判定型のEAが複数ある
既刊のMACD・ストキャスティクス・ボリンジャーバンド・アリゲーター等のEA記事は、
いずれも「いつ入るか」をテーマにしたエントリー条件の記事でした。
この記事は「入った後どう決済するか」をテーマにしているため、
エントリー条件自体は最も基本的な移動平均クロスに留め、決済管理のロジックに紙面を使います。
部分決済のメリット
・全部を一度に利確すると、その後さらに伸びた利益を取り逃す
・全部を持ち続けると、伸びた利益が逆転で失われるリスクが残る
・一部だけ確定し、残りを伸ばすことで両方のリスクを分散できる
本記事の決済ルール(2段階)
| 段階 | 条件 | 動作 |
|---|---|---|
| 第1段階 | 利益がInpStage1Pipsに到達 | 保有量の一部を決済し、SLを建値へ移動 |
| 第2段階 | 利益がInpStage2Pipsに到達 | 残りの一部をさらに決済 |
発注条件(シンプルな移動平均クロス)
決済管理の土台になる、最小限のエントリー条件です。
ゴールデンクロス・デッドクロスでエントリー
double fastNow = iMA(_Symbol, 0, InpFastMA, 0, MODE_SMA, PRICE_CLOSE, 1); double slowNow = iMA(_Symbol, 0, InpSlowMA, 0, MODE_SMA, PRICE_CLOSE, 1); double fastPrev = iMA(_Symbol, 0, InpFastMA, 0, MODE_SMA, PRICE_CLOSE, 2); double slowPrev = iMA(_Symbol, 0, InpSlowMA, 0, MODE_SMA, PRICE_CLOSE, 2); bool goldenCross = (fastPrev <= slowPrev && fastNow > slowNow); bool deadCross = (fastPrev >= slowPrev && fastNow < slowNow);
損切りは発注時点で必ず設定する
このサイトで繰り返し指摘している通り、`sl`は発注と同時に必ず計算し、
未設定のまま放置しません。
第1段階: 一部を決済してブレイクイーブンへ
利益が一定pipsに達したときの、最初の決済です。
利益をpipsに換算する
double point = _Point; if(_Digits == 3 || _Digits == 5) point *= 10; double profitPips = 0; if(OrderType() == OP_BUY) profitPips = (Bid - OrderOpenPrice()) / point; else if(OrderType() == OP_SELL) profitPips = (OrderOpenPrice() - Ask) / point;
OrderCloseで一部だけ決済する
double closeLots = NormalizeLotsToStep(OrderLots() * (InpStage1Percent / 100.0));
double closePrice = (OrderType() == OP_BUY) ? Bid : Ask;
if(closeLots >= MarketInfo(_Symbol, MODE_MINLOT) &&
closeLots < OrderLots())
{
OrderClose(OrderTicket(), closeLots, closePrice, 3, clrYellow);
}
lotsに全量より少ない数量を渡すことで、注文の一部だけを決済できます。
決済と同時にストップロスを建値へ移動する
OrderModify(OrderTicket(), OrderOpenPrice(), OrderOpenPrice(),
OrderTakeProfit(), 0, clrBlue);
一部を確保したら、残りの注文の損切りラインを建値まで引き上げ(買いの場合)、
これ以上は「負けない」状態にします。
第2段階: さらに決済してトレーリングへ切り替え
利益がさらに伸びたときの、2回目の決済と管理方法です。
第2段階の判定とロット残量チェック
double remainingLots = OrderLots();
double closeLots2 = NormalizeLotsToStep(remainingLots * (InpStage2Percent / 100.0));
if(closeLots2 >= MarketInfo(_Symbol, MODE_MINLOT) &&
closeLots2 < remainingLots)
{
OrderClose(OrderTicket(), closeLots2, closePrice, 3, clrYellow);
}
else
{
// 残量が最小ロットを下回る場合は全決済に切り替える
OrderClose(OrderTicket(), remainingLots, closePrice, 3, clrYellow);
}
ブローカーのMODE_LOTSTEP/MODE_MINLOTによっては、
計算上のロットが最小単位を下回ることがあるため、
その場合は無理に部分決済せず全決済に切り替えます。
残りはトレーリングストップに切り替える
double newSl = (OrderType() == OP_BUY)
? Bid - InpTrailPips * point
: Ask + InpTrailPips * point;
bool improves = (OrderType() == OP_BUY) ? (newSl > OrderStopLoss())
: (newSl < OrderStopLoss());
if(improves)
OrderModify(OrderTicket(), OrderOpenPrice(), newSl, OrderTakeProfit(), 0, clrBlue);
第2段階が終わった残りは、
これ以上有利な方向にしか動かさないガード条件つきの、
トレーリングストップで伸ばします。
GlobalVariableでステージを記録し、二重実行を防ぐ
if(!GlobalVariableCheck("PCSE_S1_" + (string)OrderTicket()))
{
// 第1段階の処理
GlobalVariableSet("PCSE_S1_" + (string)OrderTicket(), 1);
}
チケット番号をキーにしたGlobalVariableで、各段階が実行済みかどうかを記録し、
同じ段階を二重に実行しないようにします。
・GlobalVariableは端末を再起動しても残る(消えない)
・この性質を使い、EAを一度外して再適用しても段階の記録を保持する
・そのためOnDeinit()では一括削除しない(削除すると段階が消え、
・既に部分決済した位置から段階がやり直されてしまう)
決済済みのチケットのGlobalVariableは、
ポジションが完全に決済された直後(第2段階で残量を全決済したとき)にだけ削除します。
実際に動くコード全体
ここまでの要素をまとめた、実際にMetaEditorでコンパイルできるコードです。
//+------------------------------------------------------------------+
//| PartialCloseScaleOutEA.mq4 |
//| Copyright 2026, FX-EA System Project Creator |
//| https://creator.fx-ea-system-project.com/ |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, FX-EA System Project Creator"
#property link "https://creator.fx-ea-system-project.com/"
#property version "1.00"
#property strict
input int InpFastMA = 10; //短期移動平均の期間
input int InpSlowMA = 30; //長期移動平均の期間
input double InpLots = 0.30; //発注ロット数(部分決済のため少し多めを推奨)
input int InpStopLossPips = 300; //初期損切り(pips)
input int InpStage1Pips = 150; //第1段階の利益しきい値(pips)
input double InpStage1Percent = 50.0; //第1段階で決済する割合(%)
input int InpStage2Pips = 300; //第2段階の利益しきい値(pips)
input double InpStage2Percent = 50.0; //第2段階で決済する割合(残量に対する%)
input int InpTrailPips = 150; //第2段階後のトレーリング幅(pips)
input int InpMagic = 20261114; //マジックナンバー
datetime lastBarTime = 0;
double NormalizeLotsToStep(double lots)
{
double step = MarketInfo(_Symbol, MODE_LOTSTEP);
double minLot = MarketInfo(_Symbol, MODE_MINLOT);
double rounded = MathFloor(lots / step) * step;
if(rounded < minLot)
rounded = 0;
return(NormalizeDouble(rounded, 2));
}
double GetProfitPips(double point)
{
if(OrderType() == OP_BUY)
return((Bid - OrderOpenPrice()) / point);
if(OrderType() == OP_SELL)
return((OrderOpenPrice() - Ask) / point);
return(0);
}
void ManagePosition(double point)
{
double profitPips = GetProfitPips(point);
string ticketStr = (string)OrderTicket();
double closePrice = (OrderType() == OP_BUY) ? Bid : Ask;
if(profitPips >= InpStage1Pips && !GlobalVariableCheck("PCSE_S1_" + ticketStr))
{
double closeLots = NormalizeLotsToStep(OrderLots() * (InpStage1Percent / 100.0));
if(closeLots > 0 && closeLots < OrderLots())
{
if(OrderClose(OrderTicket(), closeLots, closePrice, 3, clrYellow))
{
OrderModify(OrderTicket(), OrderOpenPrice(), OrderOpenPrice(),
OrderTakeProfit(), 0, clrBlue);
}
}
GlobalVariableSet("PCSE_S1_" + ticketStr, 1);
return;
}
if(profitPips >= InpStage2Pips && GlobalVariableCheck("PCSE_S1_" + ticketStr) &&
!GlobalVariableCheck("PCSE_S2_" + ticketStr))
{
double remainingLots = OrderLots();
double closeLots2 = NormalizeLotsToStep(remainingLots * (InpStage2Percent / 100.0));
if(closeLots2 > 0 && closeLots2 < remainingLots)
{
OrderClose(OrderTicket(), closeLots2, closePrice, 3, clrYellow);
GlobalVariableSet("PCSE_S2_" + ticketStr, 1);
}
else
{
// 残量が最小ロットを下回るため全決済し、このチケットの記録を削除する
OrderClose(OrderTicket(), remainingLots, closePrice, 3, clrYellow);
GlobalVariableDel("PCSE_S1_" + ticketStr);
GlobalVariableDel("PCSE_S2_" + ticketStr);
}
return;
}
if(GlobalVariableCheck("PCSE_S2_" + ticketStr) && OrderSelect(OrderTicket(), SELECT_BY_TICKET))
{
double newSl = (OrderType() == OP_BUY)
? Bid - InpTrailPips * point
: Ask + InpTrailPips * point;
bool improves = (OrderType() == OP_BUY) ? (newSl > OrderStopLoss())
: (newSl < OrderStopLoss());
if(improves)
OrderModify(OrderTicket(), OrderOpenPrice(), newSl, OrderTakeProfit(), 0, clrBlue);
}
}
bool HasOpenPosition()
{
for(int i = 0; i < OrdersTotal(); i++)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES) &&
OrderSymbol() == _Symbol && OrderMagicNumber() == InpMagic)
return(true);
}
return(false);
}
void OnTick()
{
double point = _Point;
if(_Digits == 3 || _Digits == 5)
point *= 10;
for(int i = 0; i < OrdersTotal(); i++)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES) &&
OrderSymbol() == _Symbol && OrderMagicNumber() == InpMagic)
ManagePosition(point);
}
if(Time[0] == lastBarTime)
return;
lastBarTime = Time[0];
if(Bars < MathMax(InpFastMA, InpSlowMA) + 5)
return;
if(HasOpenPosition())
return;
double fastNow = iMA(_Symbol, 0, InpFastMA, 0, MODE_SMA, PRICE_CLOSE, 1);
double slowNow = iMA(_Symbol, 0, InpSlowMA, 0, MODE_SMA, PRICE_CLOSE, 1);
double fastPrev = iMA(_Symbol, 0, InpFastMA, 0, MODE_SMA, PRICE_CLOSE, 2);
double slowPrev = iMA(_Symbol, 0, InpSlowMA, 0, MODE_SMA, PRICE_CLOSE, 2);
bool goldenCross = (fastPrev <= slowPrev && fastNow > slowNow);
bool deadCross = (fastPrev >= slowPrev && fastNow < slowNow);
if(goldenCross)
{
double sl = Bid - InpStopLossPips * point;
int ticket = OrderSend(_Symbol, OP_BUY, InpLots, Ask, 3, sl, 0,
"PartialCloseScaleOutEA", InpMagic, 0, clrBlue);
if(ticket < 0)
Print("買い発注に失敗しました。エラーコード: ", GetLastError());
}
else if(deadCross)
{
double sl = Ask + InpStopLossPips * point;
int ticket = OrderSend(_Symbol, OP_SELL, InpLots, Bid, 3, sl, 0,
"PartialCloseScaleOutEA", InpMagic, 0, clrRed);
if(ticket < 0)
Print("売り発注に失敗しました。エラーコード: ", GetLastError());
}
}
// OnDeinit()でPCSE_変数を一括削除しない。EAの再適用時にも
// 段階の記録を保持するため、削除は第2段階の全決済時のみ行う。
//+------------------------------------------------------------------+
利益が伸びるたびに、段階的にポジションが減り、
最終的には1本のトレーリングストップだけが残る構成です。
MetaEditorでのコンパイル・バックテスト手順
実際に動かすまでの手順です。
- MetaEditorで新規Expert Advisor「PartialCloseScaleOutEA」を作成し、上のコードを貼り付ける
- F7キー(またはコンパイルボタン)でコンパイルし、「0 error(s), 0 warning(s)」になることを確認する
- ストラテジーテスターでこのEAを選択し、任意の通貨ペア・期間でバックテストを実行する
- 「結果」タブの注文履歴で、1つのエントリーに対して2回の部分決済と1回の最終決済(同じチケット番号で複数行)が記録されていることを確認する
よくあるエラーと注意点
実装・運用でつまずきやすいポイントをまとめます。
部分決済後も履歴に同じチケット番号が複数回残る
・部分決済をすると、決済した分が履歴(OrdersHistoryTotal側)に
・同じチケット番号で追加される
・残った分は同じチケット番号のまま
・ロット数だけ減って保有中(OrdersTotal側)に残り続ける
・集計スクリプトでチケット番号だけを重複排除すると、決済した分の損益を見落とす
最小ロットに満たない部分決済はエラーになる
MarketInfo(MODE_LOTSTEP)やMODE_MINLOTは、
ブローカーごとに異なり、
計算上のclosePercentが小さすぎると、
NormalizeLotsToStep()の結果が0になります。
このEAは0のときは部分決済を行わず、
第2段階では残り全部の決済に切り替える安全策を入れています。
損切りで決済された場合はGlobalVariableが残る
ストップロスにヒットして決済された場合、
このEA自身はOrderCloseを呼んでいないため、
そのチケットのPCSE_変数は削除されず残ります。
実害は無いものの(新しいチケット番号で判定するため動作に影響しない)、
MT4の「グローバル変数」ウィンドウには使われなくなった記録が蓄積します。
気になる場合は、定期的にMT4の「ツール」→「グローバル変数」から手動で整理してください。
ブレイクイーブンへの移動でもスプレッド分は残る
OrderModifyでSLをOrderOpenPrice()へ移動しても、
買い注文はAskで入ってBidで決済されるため、
実際に建値で決済してもスプレッド分はわずかに損失になります。
厳密に「絶対に負けない」ラインにしたい場合は、
スプレッド分を加味してさらに有利な位置に、
SLを置く調整が必要です。
よくある質問
パーシャルクローズEAについて、よくある疑問に回答します。
第1段階と第2段階の割合は変更できますか?
できます。
InpStage1Percent・InpStage2Percentの入力値を変えるだけで、
決済する割合を自由に調整できます。
エントリー条件を他のEAのロジックに差し替えられますか?
できます。
OnTick内のゴールデンクロス・デッドクロスの判定部分を、
既刊の他のEA記事の発注条件に置き換えれば、
ManagePosition()の部分決済ロジックはそのまま使えます。
1つのポジションを3段階以上に分けることはできますか?
できます。
GlobalVariableのキー(“PCSE_S3_”等)と判定の分岐を増やせば、
同じ考え方で3段階目・4段階目を追加できます。
【MQL4】含み益を2段階で確定する「パーシャルクローズ(部分決済)」EAの作り方(OrderCloseで一部だけ決済する):まとめ
この記事では、OrderCloseのlots引数に全量より少ない数量を渡すことで、
含み益が出たポジションを一度に利確するのではなく、
2段階に分けて部分的に決済するEAを解説しました。
第1段階で一部を確保してストップロスを建値へ移動し、
第2段階でさらに一部を確保してトレーリングストップに切り替えるという、
このサイトでまだ扱っていなかった決済管理の設計を実装しています。
GlobalVariableでチケットごとの段階を記録することで、
同じ段階を二重に実行しない安全な管理ができる点も、
既刊globalvariableset-get-mt4.mdの実例として参考にできます。
コメント