開倉時設好的停損停利,之後就不動了:要嘛碰停損賠一筆固定的錢,要嘛碰停利賺一筆固定的錢。這篇加兩個常見的出場:保本是賺到一定程度就把停損搬到進場價附近,之後最差是平手;移動停損是停損跟著價格走,賺到的鎖一部分。兩個做法的共同規則只有一句:停損只往獲利方向搬,永遠不往回搬。
範例 MaCrossTrail.mq5 是餘額 1% 風險那隻均線交叉 EA(MaCrossRisk.mq5)加一個 ManagePosition(),每根新 K 線先看持倉的停損要不要搬、再看訊號;開倉多了「停利倍數 0 就不設」的處理,魔術碼另外換了一個,其他沒動。整份在我的 MT5 上編譯過,策略測試器用 2025 年 1 月的 EURUSD H1 跑了四種設定,文中日誌和數字是實際輸出。
先把範例跑起來
- 把 MaCrossTrail.mq5 放到
MQL5\Experts\,在 MetaEditor 打開、按F7,0 errors。 - 策略測試器
Ctrl+R:EA 選 MaCrossTrail,EURUSD,H1,日期 2025.01.02 到 2025.01.31,模式「每次報價」,入金 10000,杠杆 1:100,按「開始」。 - 看「日誌」頁。每筆開倉之後多了「移動」「保本」兩種行,印停損從哪搬到哪;測試器自己也會對每次修改印一行 position modified。
1. 參數與交易物件
//=== 1. 參數與交易物件 ==============================================
#include <Trade\Trade.mqh>
input int i_FastPeriod = 10; // 快線期數
input int i_SlowPeriod = 30; // 慢線期數
input ENUM_MA_METHOD i_MaMethod = MODE_EMA; // 均線種類
input int i_AtrPeriod = 14; // ATR 期數
input double i_RiskPct = 1.0; // 每筆風險(餘額的 %)
input double i_MaxLots = 1.0; // 手數上限
input double i_SlAtr = 2.0; // 停損 = ATR x 倍數
input double i_TpAtr = 3.0; // 停利倍數,0 不設
input double i_BeAtr = 1.0; // 保本門檻倍數,0 不用
input int i_BeLockPts = 10; // 保本時多鎖幾點
input double i_TrailAtr = 2.0; // 移動停損倍數,0 不用
input long i_Magic = 20250701; // 魔術碼
CTrade g_Trade;
int g_FastHandle = INVALID_HANDLE;
int g_SlowHandle = INVALID_HANDLE;
int g_AtrHandle = INVALID_HANDLE;
datetime g_LastBarTime = 0;多三個參數。i_BeAtr:賺到幾倍 ATR 就保本,保本價是進場價再往獲利方向多 i_BeLockPts 點,把手續費蓋掉。i_TrailAtr:移動停損跟現價保持幾倍 ATR 的距離。兩個都設 0 就是關掉,跑出來會跟 MaCrossRisk 一模一樣,文末的對照就是這樣跑的。i_TpAtr 也多了一個意思:設 0 就不掛停利,讓移動停損自己決定什麼時候出。
指標 handle、算手數、查保證金、找持倉那幾段都沒改,檔案裡有,這篇不貼。
2. 開倉:停利倍數 0 就不設
bool useTp = (i_TpAtr > 0);
...
if(useTp && tpDist < minDist) tpDist = minDist;
...
double tp = 0; // 0 = 不設停利
if(useTp)
tp = NormalizeDouble(isBuy ? ask + tpDist : bid - tpDist,
_Digits);CTrade 的 Buy()/Sell() 停利傳 0 就是不設。原本的寫法直接把 i_TpAtr 設 0 拿不到這個效果:停利距離 0 會被「不能小於 StopsLevel」那行補成最小距離,掛出一個貼著現價的停利。所以補正那行和算 tp 那幾行都看 useTp。
3. 保本與移動停損
//=== 3. 保本與移動停損:算出想要的停損價,比現在的好才改 ============
void ManagePosition(ulong ticket, int type, double atr)
{
if(!PositionSelectByTicket(ticket))
return;
double open = PositionGetDouble(POSITION_PRICE_OPEN);
double sl = PositionGetDouble(POSITION_SL);
double tp = PositionGetDouble(POSITION_TP);
bool isBuy = (type == POSITION_TYPE_BUY);
double dir = isBuy ? 1 : -1; // 多 +1、空 -1
double price = SymbolInfoDouble(_Symbol, isBuy ? SYMBOL_BID
: SYMBOL_ASK);
double gain = dir * (price - open); // 現在賺的價差
// 兩個候選:保本價、移動停損價,留對自己最有利的
double want = sl; // 目前的停損
if(sl == 0) // 沒停損:從最差起算
want = isBuy ? 0 : DBL_MAX;
string why = "";
if(i_BeAtr > 0 && gain >= atr * i_BeAtr)
{
double be = open + dir * i_BeLockPts * _Point;
if(dir * (be - want) > 0) { want = be; why = "保本"; }
}
if(i_TrailAtr > 0)
{
double tr = price - dir * atr * i_TrailAtr;
if(dir * (tr - want) > 0) { want = tr; why = "移動"; }
}
want = NormalizeDouble(want, _Digits);
if(why == "")
return; // 沒有比現在好
if(sl != 0 && dir * (want - sl) < _Point * 0.5)
return; // 好不到 1 點,不改
// 新停損離現價至少要 StopsLevel,太近伺服器會拒
long stops = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
if(dir * (price - want) < stops * _Point)
return;
if(!g_Trade.PositionModify(ticket, want, tp) ||
g_Trade.ResultRetcode() != TRADE_RETCODE_DONE)
{
Print("改停損失敗 ", g_Trade.ResultRetcode(), " ",
g_Trade.ResultRetcodeDescription());
return;
}
Print(why, " #", ticket, " 停損 ", DoubleToString(sl, _Digits),
" -> ", DoubleToString(want, _Digits),
" 現價 ", DoubleToString(price, _Digits));
}dir 是 +1(多)或 −1(空),乘上去之後多單空單共用同一套式子:dir * (a - b) > 0 就是「a 比 b 對自己更有利」,多單是 a 比 b 高、空單是 a 比 b 低。gain 是現在賺的價差,用平倉會用到的那邊的價,多單看 Bid、空單看 Ask。
接著算兩個候選停損價:保本價(賺夠了才有)和移動停損價(現價往回退 ATR 倍數),want 從現在的停損起算,候選價要比它更有利才換上去。那兩個 if 就是停損只往獲利方向搬:候選價比現在的停損差,就什麼都不做。差不到 1 點的也不改,省得每根 K 線都丟一筆修改給伺服器。
改之前看 StopsLevel:新停損離現價要有那個距離,太近伺服器會拒。然後 PositionModify(ticket, sl, tp) 改停損,停利要把原值一起傳回去,傳 0 會把原本的停利拿掉。
這個函式沒有記住任何東西:有沒有保本過、上次搬到哪,全部從持倉本身讀回來。EA 重啟、換台電腦接著跑,看到的持倉是什麼就從什麼開始。用一個「已經保本」的布林變數也做得到,但重啟就丟了。
4. OnTick:先管持倉、再看訊號
//=== 4. OnTick:新 K 線先管持倉、再看訊號 ==========================
void OnTick()
{
ShowStatus();
if(!IsNewBar())
return;
double fast[], slow[], atr[];
ArraySetAsSeries(fast, true);
ArraySetAsSeries(slow, true);
ArraySetAsSeries(atr, true);
if(CopyBuffer(g_FastHandle, 0, 0, 3, fast) < 3 ||
CopyBuffer(g_SlowHandle, 0, 0, 3, slow) < 3 ||
CopyBuffer(g_AtrHandle, 0, 0, 3, atr) < 3)
{
Print("指標資料還沒準備好,錯誤碼 ", GetLastError());
g_LastBarTime = 0;
return;
}
ulong ticket = 0;
int have = FindMyPosition(ticket);
if(have >= 0)
ManagePosition(ticket, have, atr[1]);一根 K 線只看一次:新 K 線的第一個 tick,用上一根收完的 ATR 和那時的現價算。想要每個 tick 都跟著搬,得把 CopyBuffer 和這個呼叫搬到 IsNewBar() 前面,而且「差不到 1 點不改」要放寬,不然一秒鐘會丟好幾筆修改。
移動停損的距離用當下的 ATR,波動縮小距離跟著縮,所以會出現價格沒漲、停損卻往上收的情況,下面的日誌有例子。要固定距離就在開倉時把 ATR 記下來。
執行結果
EURUSD 2025 年 1 月,預設參數(保本 1 ATR、移動停損 2 ATR、停利 3 ATR),14 筆,進場時間和價格跟 MaCrossRisk 逐筆相同,手數因餘額不同差一步。拿 #6 這筆看整個過程:
開多 #6 0.27 手 @1.02483 停損 380 點 風險 102.60
移動 #6 停損 1.02103 -> 1.02123 現價 1.02490
移動 #6 停損 1.02123 -> 1.02148 現價 1.02487
移動 #6 停損 1.02148 -> 1.02169 現價 1.02504
移動 #6 停損 1.02169 -> 1.02220 現價 1.02525
移動 #6 停損 1.02220 -> 1.02233 現價 1.02543
移動 #6 停損 1.02233 -> 1.02290 現價 1.02618
保本 #6 停損 1.02290 -> 1.02493 現價 1.02680
移動 #6 停損 1.02493 -> 1.02613 現價 1.02949
移動 #6 停損 1.02613 -> 1.02635 現價 1.02992開多之後停損跟著往上搬了 6 次,賺到 1 ATR 時跳到保本價 1.02493(進場 1.02483 加 10 點),之後再跟 2 次,最後碰停利出場。
第 2、3 行就是上面說的情況:現價從 1.02490 掉到 1.02487,停損卻從 1.02123 搬到 1.02148,因為這根的 ATR 比上一根小。
整個月 EA 印了 62 行修改,測試器日誌也有 62 行 position modified,一一對應。修改不是成交,報告的交易總計還是 14。
同一個月、同一套訊號,四種設定的 14 筆各怎麼結束。「獲利側」是停損已經搬過進場價才被打到,也就是保本或移動停損收掉的;「虧損側」是停損還在進場價另一邊;「反向/測試結束」是被反向訊號平掉、或測試結束時強制平倉:
| 設定 | 停利 | 停損在獲利側 | 停損在虧損側 | 反向/測試結束 |
|---|---|---|---|---|
| 固定停損停利(對照) | 7 | 0 | 2 | 5 |
| +保本 | 5 | 3 | 2 | 4 |
| +保本+移動停損 | 5 | 3 | 3 | 3 |
| +保本+移動停損,不設停利 | 0 | 8 | 3 | 3 |
報告數字:
| 設定 | 總淨盈利 (USD) | 盈利因子 | 最大權益數虧損 |
|---|---|---|---|
| 固定停損停利(對照) | 656.51 | 2.51 | 441.66 (4.19%) |
| +保本 | 387.88 | 1.96 | 461.90 (4.38%) |
| +保本+移動停損 | 415.99 | 2.11 | 433.79 (4.11%) |
| +保本+移動停損,不設停利 | 197.57 | 1.53 | 435.01 (4.15%) |
三種改法這個月都輸給固定停損停利。只加保本那趟:8 筆保本過,3 筆最後在保本價出場。其中 #18、#20 原本是到停利各賺 148.20、152.64 塊,現在先回檔碰到保本價,各賺 3.90、4.70 塊(正好 10 點乘手數,扣掉手續費還是正的);#8 原本是被反向訊號平掉賠 28.08,變成賺 3.60。對照趟碰停損的 2 筆,保本一筆也沒救到,一樣賠。
再加移動停損:#14 開倉時印的風險是 102.48,停損先跟上來一段才被打到,賠 66.49;#16 原本是被反向訊號平掉賠 59.07,停損跟上來之後在虧損側被打到,賠 66.99。一筆少賠、一筆變糟。
不設停利那趟跟上一趟只差停利,最差。上一趟到停利的 5 筆,這趟全部變成在獲利側被移動停損收掉:只有 #6 賺更多(153.90 變 173.42),其他 4 筆各少 40 到 78 塊;其餘 9 筆出場一樣。價格每次都到了原本停利的位置又回頭,要退 2 ATR 才出場,退掉的就是那段。1 月的 EURUSD 沒有那種一路走不回頭的趨勢,「讓獲利跑」跑不出去。
這不是保本和移動停損沒用的結論,是 14 筆的一個月看不出來。它們改變的是每一筆怎麼結束,整體是好是壞看行情有沒有給趨勢。要不要用、距離幾倍 ATR,把同一段長一點的資料四種設定各跑一次比報告,不要憑直覺覺得保本一定比較安全。
本篇檔案:MaCrossTrail.mq5,放到 MQL5\Experts\ 底下,在 MetaEditor 打開按 F7 就能用。