固定 0.10 手有個問題:停損距離每筆不一樣,賠的錢也就每筆不一樣。這篇把手數改成由停損距離倒推,每筆最多賠餘額的 1%,順便講算手數要用到的商品規格,還有下單前怎麼確認保證金夠不夠。
範例 MaCrossRisk.mq5 是把固定 0.10 手的均線交叉 EA(MaCrossTrade.mq5:EMA 10/30 交叉進場、ATR 倍數停損停利、反向訊號先平再開)只換掉手數這一塊,魔術碼另外換了一個,其他都沒動。整份在我的 MT5 上編譯過、策略測試器跑過 2025 年 1 月的 EURUSD H1 和另外幾種情境,文中日誌和數字是實際輸出。
先把範例跑起來
- 把 MaCrossRisk.mq5 放到
MQL5\Experts\,在 MetaEditor 打開、按F7,0 errors。 - 策略測試器
Ctrl+R:EA 選 MaCrossRisk,EURUSD,H1,日期 2025.01.02 到 2025.01.31,模式「每次報價」,入金 10000,杠杆 1:100,按「開始」。 - 看「日誌」頁。開頭兩行「規格」是這個商品算手數要用的數字,之後每筆開倉都印手數、停損幾點、這筆最多賠多少。
固定手數,其實不知道自己在賠多少
同一個月、同一套訊號,固定 0.10 手的 MaCrossTrade 停損距離從 161 點到 434 點,一筆碰停損賠 16.1 到 43.4 塊,是 10000 塊帳戶的 0.16% 到 0.43%。差快三倍,而且波動大的時候停損拉遠、賠得更多,剛好是最不該加碼的時候。
反過來想:先決定一筆最多賠多少,比如餘額的 1%,再用停損距離算手數。停損遠就開小、停損近就開大,每筆賠的錢一樣。
算手數要用的商品規格
一手賠多少錢,看三個規格:SYMBOL_TRADE_TICK_SIZE 是價格最小跳動一格(tick size,下面簡稱 tick),SYMBOL_TRADE_TICK_VALUE_LOSS 是一手跳一格賠多少(通常是帳戶幣別,已經換算過),SYMBOL_TRADE_CONTRACT_SIZE 是一手的合約量。
手數本身有最小、最大、步進三個限制,SYMBOL_VOLUME_MIN、SYMBOL_VOLUME_MAX、SYMBOL_VOLUME_STEP,不照規格送單會被拒,錯誤碼 10014。範例 EA 啟動時把這些印出來,三個商品在我的 Tickmill 帳號上:
| 商品 | 合約 | tick | 每 tick (USD) | 最小/步進 (手) |
|---|---|---|---|---|
| EURUSD | 100000 | 0.00001 | 1.0000 | 0.01/0.01 |
| XAUUSD | 100 | 0.01 | 1.0000 | 0.01/0.01 |
| USDJPY | 100000 | 0.001 | 0.6357 | 0.01/0.01 |
EURUSD 一手 100000 歐元、跳 0.00001 賠 1 塊;黃金一手 100 盎司、跳 0.01 也是 1 塊;USDJPY 一手跳 0.001 是 100 日圓,換成美金 0.6357 塊,這個數跟著匯率走,所以每次算手數都要重新讀。
tick 不一定等於 _Point:有些商品一跳是好幾個 point,例如指數期貨報價到 0.01、一跳卻是 0.25,算錢要用 tick,不能用 _Point。這裡三個商品剛好相等,別的商品、別家券商不一定。
這些數字在 MT5 主視窗也看得到:「市場報價」(Ctrl+M)對商品按右鍵、「規格」。
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; // 停利 = ATR x 倍數
input long i_Magic = 20250601; // 魔術碼
CTrade g_Trade;
int g_FastHandle = INVALID_HANDLE;
int g_SlowHandle = INVALID_HANDLE;
int g_AtrHandle = INVALID_HANDLE;
datetime g_LastBarTime = 0;原本固定手數的 i_Lots 換成 i_RiskPct(每筆賠餘額的百分之幾)和 i_MaxLots(算出來再大也不超過這個)。上限是保險:餘額大、停損又很近的時候,算出來的手數可能大到不合理。
2. OnInit:把規格印出來
//=== 2. OnInit:建指標、印出商品規格 ================================
int OnInit()
{
g_FastHandle = iMA(_Symbol, _Period, i_FastPeriod, 0,
i_MaMethod, PRICE_CLOSE);
g_SlowHandle = iMA(_Symbol, _Period, i_SlowPeriod, 0,
i_MaMethod, PRICE_CLOSE);
g_AtrHandle = iATR(_Symbol, _Period, i_AtrPeriod);
if(g_FastHandle == INVALID_HANDLE ||
g_SlowHandle == INVALID_HANDLE ||
g_AtrHandle == INVALID_HANDLE)
{
Print("指標建立失敗,錯誤碼 ", GetLastError());
return INIT_FAILED;
}
g_Trade.SetExpertMagicNumber(i_Magic);
g_Trade.SetDeviationInPoints(20);
g_Trade.SetTypeFillingBySymbol(_Symbol);
// 算手數要用到的規格,先印出來看
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double stepLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double tickSz = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double tickVal = SymbolInfoDouble(_Symbol,
SYMBOL_TRADE_TICK_VALUE_LOSS);
Print("規格 ", _Symbol, " 最小 ", DoubleToString(minLot, 2),
" 手 最大 ", DoubleToString(maxLot, 2),
" 手 步進 ", DoubleToString(stepLot, 2), " 手");
Print(" 合約 ", DoubleToString(SymbolInfoDouble(_Symbol,
SYMBOL_TRADE_CONTRACT_SIZE), 0),
" tick ", DoubleToString(tickSz, _Digits),
" 每 tick ", DoubleToString(tickVal, 4), " ",
AccountInfoString(ACCOUNT_CURRENCY));
Print("EA 啟動:每筆風險 ", DoubleToString(i_RiskPct, 1),
"% 餘額,上限 ", DoubleToString(i_MaxLots, 2), " 手");
return INIT_SUCCEEDED;
}指標 handle 和 CTrade 的三行設定跟 MaCrossTrade 一樣。多的是把上面那幾個規格讀出來印一遍,用 DoubleToString() 指定小數位,tick 用商品的 _Digits 位才看得到 0.00001。實盤前看一眼這兩行,規格跟想的不一樣馬上知道。
3. 依風險比例算手數
//=== 3. 依風險比例算手數 ============================================
// slDist 是停損距離(價格差)。回傳手數,算不出可用的手數就回 0;
// risk 帶回這筆手數真正會賠的錢(捨去到步進之後)
double CalcLots(double slDist, double &risk)
{
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskMoney = balance * i_RiskPct / 100.0; // 這筆最多賠多少
double tickSize = SymbolInfoDouble(_Symbol,
SYMBOL_TRADE_TICK_SIZE);
double tickValue = SymbolInfoDouble(_Symbol,
SYMBOL_TRADE_TICK_VALUE_LOSS);
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double stepLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
risk = 0;
if(tickSize <= 0 || tickValue <= 0 || stepLot <= 0 || slDist <= 0)
{
Print("規格讀不到或停損距離為 0,這筆不做");
return 0;
}
double lossPerLot = slDist / tickSize * tickValue; // 1 手賠多少
double lots = riskMoney / lossPerLot;
long steps = (long)MathFloor(lots / stepLot + 1e-9); // 捨去到步進
lots = NormalizeDouble(steps * stepLot, 8); // 修浮點尾數
if(lots < minLot)
{
Print("風險 ", DoubleToString(riskMoney, 2), " 連最小手數 ",
DoubleToString(minLot, 2), " 手都不夠,這筆不做");
return 0;
}
lots = MathMin(lots, MathMin(maxLot, i_MaxLots));
risk = lots * lossPerLot;
return lots;
}四步:餘額乘以風險比例是這筆最多賠的錢;停損距離除以 tick 是停損有幾格,乘每格的錢是一手碰停損賠多少;兩者相除是手數;最後 MathFloor() 無條件捨去到步進,捨去不能四捨五入,進位就超過設定的風險了。
捨去之後如果連最小手數都不到,這筆就不做,回傳 0。硬開 0.01 手等於賠超過設定的比例,小帳戶會常遇到,下面有實跑的例子。算出來的手數再跟商品最大手數和 i_MaxLots 取小;risk 用參考帶回這筆真正會賠的錢,印出來對帳用,它不含手續費、隔夜利息和滑點。
除法加的 1e-9 和 NormalizeDouble() 都是在擋浮點數:0.29 除以 0.01 會算成 28.999…、29 乘 0.01 會變 0.29000000000000004,不修掉就可能少一步或被券商拒。
4. 開倉前先問保證金
//=== 4. 開倉前先問保證金夠不夠 ======================================
bool EnoughMargin(ENUM_ORDER_TYPE type, double lots, double price)
{
double need = 0;
if(!OrderCalcMargin(type, _Symbol, lots, price, need))
{
Print("OrderCalcMargin 失敗,錯誤碼 ", GetLastError());
return false;
}
double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
if(need > freeMargin)
{
Print("保證金不夠:", DoubleToString(lots, 2), " 手要 ",
DoubleToString(need, 2), ",可用 ",
DoubleToString(freeMargin, 2), ",這筆不做");
return false;
}
return true;
}OrderCalcMargin() 問這張單要押多少保證金,ACCOUNT_MARGIN_FREE 是帳戶現在還能押多少。不夠就不送,省得被伺服器退 10019(not enough money)。入金 10000、1:100 的回測碰不到這條;實盤同時跑好幾隻 EA、或券商調低槓桿時才會,文末有把杠杆改 1:1 逼它出現的例子。
5. 開倉:先算手數、再查保證金、才下單
//=== 5. 開倉:先算手數、再查保證金、才下單 ==========================
bool OpenPosition(int dir, double atr)
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double slDist = atr * i_SlAtr;
double tpDist = atr * i_TpAtr;
long stops = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
double minDist = stops * _Point + (ask - bid);
if(slDist < minDist) slDist = minDist;
if(tpDist < minDist) tpDist = minDist;
// 先把停損價修到商品的小數位,再用修過的價差算手數
bool isBuy = (dir == POSITION_TYPE_BUY);
double entry = isBuy ? ask : bid;
double sl = NormalizeDouble(isBuy ? ask - slDist : bid + slDist,
_Digits);
double tp = NormalizeDouble(isBuy ? ask + tpDist : bid - tpDist,
_Digits);
slDist = MathAbs(entry - sl);
double risk = 0;
double lots = CalcLots(slDist, risk);
if(lots <= 0)
return false;
if(!EnoughMargin(isBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL,
lots, entry))
return false;
bool ok = isBuy ? g_Trade.Buy(lots, _Symbol, 0, sl, tp)
: g_Trade.Sell(lots, _Symbol, 0, sl, tp);
if(!ok || g_Trade.ResultRetcode() != TRADE_RETCODE_DONE)
{
Print("下單失敗 ", g_Trade.ResultRetcode(), " ",
g_Trade.ResultRetcodeDescription());
return false;
}
Print(isBuy ? "開多" : "開空", " #", g_Trade.ResultOrder(),
" ", DoubleToString(lots, 2), " 手 @",
DoubleToString(g_Trade.ResultPrice(), _Digits),
" 停損 ", DoubleToString(slDist / _Point, 0), " 點",
" 風險 ", DoubleToString(risk, 2));
return true;
}這裡的順序是:先把停損停利價用 NormalizeDouble() 修到商品的小數位,再拿修過的價差去算手數。這樣印出來的「風險」就是停損真的被打到時會賠的數,不會差一個尾數。tick 比 _Point 粗的商品,價位還要再對齊 tick。OnTick、找持倉、平倉那幾段沒改,檔案裡有,這篇不貼。
執行結果
EURUSD 2025 年 1 月,14 筆,和固定 0.10 手的 MaCrossTrade 訊號、進場價、停損距離逐筆相同,只有手數不一樣。EA 印的行:
規格 EURUSD 最小 0.01 手 最大 100.00 手 步進 0.01 手
合約 100000 tick 0.00001 每 tick 1.0000 USD
EA 啟動:每筆風險 1.0% 餘額,上限 1.00 手
開多 #2 0.34 手 @1.03042 停損 288 點 風險 97.92
開空 #4 0.26 手 @1.03545 停損 389 點 風險 101.14
開多 #6 0.27 手 @1.02483 停損 380 點 風險 102.60
開空 #8 0.36 手 @1.02889 停損 284 點 風險 102.24
平倉 #8 浮動盈虧 -28.08
開多 #10 0.31 手 @1.02967 停損 331 點 風險 102.61
平倉 #10 浮動盈虧 -37.20
開空 #12 0.64 手 @1.02847 停損 161 點 風險 103.04
平倉 #12 浮動盈虧 -79.36
開多 #14 0.61 手 @1.02971 停損 168 點 風險 102.48
開空 #16 0.33 手 @1.02864 停損 304 點 風險 100.32
平倉 #16 浮動盈虧 -59.07
開多 #18 0.38 手 @1.03043 停損 260 點 風險 98.80
開空 #20 0.48 手 @1.04039 停損 212 點 風險 101.76
開多 #22 0.30 手 @1.04355 停損 340 點 風險 102.00
開空 #24 0.30 手 @1.04372 停損 347 點 風險 104.10
開多 #26 0.30 手 @1.04316 停損 352 點 風險 105.60
開空 #28 0.24 手 @1.04074 停損 434 點 風險 104.16手數從 0.24 到 0.64 手,每筆風險都在 97.92 到 105.60 塊之間。不是剛好 100 有兩個原因:捨去到步進通常會少一點;餘額會變,賺了 1% 跟著變大、賠了跟著縮,最後一筆算出來是 104.16。
碰停損的 2 筆可以對帳:#14 開 0.61 手印風險 102.48,報告成交表那筆盈利欄是 -102.48;#26 開 0.30 手印 105.60,盈利欄 -105.60。一分不差,手續費另外算,那兩筆各是 3.66 和 1.80。
| 項目 | 固定 0.10 手 | 餘額 1% |
|---|---|---|
| 交易總計 | 14 | 14 |
| 總淨盈利 (USD) | 237.28 | 656.51 |
| 最大權益數虧損 | 103.57 (1.02%) | 441.66 (4.19%) |
賺比較多不是策略變好了,是平均開了 0.37 手、三倍多的部位,回撤也跟著放大到四倍。風險比例不會讓一個策略從賠變賺,它做的是讓每一筆賠的錢可以預期,回撤跟著風險比例走而不是跟著行情。
1% 是常見的起點,連賠 10 筆掉一成;要 2% 還是 0.5%,看自己受得了多深的回撤。
換商品、換資金、換槓桿
黃金同一個月:手數只有 0.07 到 0.11 手,因為停損動輒 930 到 1353 點、一點 1 塊。手數每差一個步進 0.01 手,賠的錢就差 9 到 14 塊,捨去之後風險落在 92.24 到 104.24,比 EURUSD 離目標遠。一步的錢大的商品就是這樣,帳戶小的時候更明顯。
入金 300 塊跑 EURUSD:1% 是 3 塊,0.01 手停損 161 點要賠 1.61,開得成;停損 304 點以上的訊號 0.01 手就超過 3 塊,14 個訊號被擋掉 8 個,日誌印「連最小手數 0.01 手都不夠,這筆不做」。要改成硬開最小手數也行,那就要接受這種帳戶單筆賠到 1.4% 的事實。
杠杆改 1:1、入金 10000:第一筆 0.34 手要押 35034.28,可用只有 10000,整個月 14 個訊號全部印「保證金不夠」,一筆都沒開。沒有這個檢查的話一樣一筆都開不了,只是換成伺服器拒單:固定 0.10 手的 MaCrossTrade 在 1:1 跑同一個月,日誌是 14 行「下單失敗 10019 not enough money」。結果相同,差在自己先算過就知道差多少。
本篇檔案:MaCrossRisk.mq5,放到 MQL5\Experts\ 底下,在 MetaEditor 打開按 F7 就能用。