返回《給 CFD 交易者的 MetaTrader 5 與 MQL5》
第三部分

CFD 的部位規模(多數 EA 跳過的那堂課)

手數不是風險。把 tick value、合約規模與停損距離換成真正的部位大小。

進階閱讀時間約 13 分鐘初學 → 進階第 13 / 17 堂
在 MetaEditor 實際跑一次
正確做出風險導向的 sizing

存成 MQL5/Scripts/Lesson13Sizing.mq5。它把 tick size 與 tick value 換成 1% 風險對應的手數、拿券商的限制驗證它,並檢查保證金。

//+------------------------------------------------------------------+
//| Lesson 13 - Risk-based position sizing for CFD                   |
//+------------------------------------------------------------------+
#property copyright "Strategist Academy"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Round volume down onto the broker's step                         |
//+------------------------------------------------------------------+
double NormalizeVolumeDown(const string sym, double vol)
{
   double vMin  = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
   double vMax  = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
   double vStep = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);

   vol = MathMax(vMin, MathMin(vMax, vol));
   if(vStep > 0)
      vol = vMin + MathFloor((vol - vMin) / vStep + 0.0000001) * vStep;

   return NormalizeDouble(vol, 2);
}

//+------------------------------------------------------------------+
//| Lots such that a stop of stopPoints costs at most riskMoney      |
//+------------------------------------------------------------------+
double CalcLotsForRisk(const string sym, const double riskMoney, const double stopPoints)
{
   double tickValue = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE);
   double tickSize  = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE);

   if(tickValue <= 0 || tickSize <= 0 || stopPoints <= 0)
      return 0.0;

   // stopPoints * _Point is a PRICE distance. tickValue is per TICK, so
   // convert price distance into ticks before multiplying.
   double stopTicks  = (stopPoints * _Point) / tickSize;
   double riskPerLot = stopTicks * tickValue;

   if(riskPerLot <= 0)
      return 0.0;

   return NormalizeVolumeDown(sym, riskMoney / riskPerLot);
}

void OnStart()
{
   string sym = _Symbol;

   double equity  = AccountInfoDouble(ACCOUNT_EQUITY);
   double riskPct = 1.0;                       // risk 1% of equity
   double riskMoney = equity * riskPct / 100.0;

   double tickValue = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE);
   double tickSize  = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE);
   double contract  = SymbolInfoDouble(sym, SYMBOL_TRADE_CONTRACT_SIZE);
   long   stopsLevel= SymbolInfoInteger(sym, SYMBOL_TRADE_STOPS_LEVEL);

   Print("=== Sizing on ", sym, " ===");
   Print("Equity:        ", DoubleToString(equity, 2));
   Print("Risk budget:   ", DoubleToString(riskMoney, 2), " (", riskPct, "%)");
   Print("Tick size:     ", DoubleToString(tickSize, _Digits));
   Print("Tick value:    ", DoubleToString(tickValue, 2), " per lot per tick");
   Print("Contract size: ", DoubleToString(contract, 2));
   Print("Point:         ", DoubleToString(_Point, _Digits),
         "   (point vs tick differ? ", (MathAbs(_Point - tickSize) > 0.0000000001 ? "YES" : "no"), ")");
   Print("Stops level:   ", stopsLevel, " points");
   Print("Swap long/short: ", DoubleToString(SymbolInfoDouble(sym, SYMBOL_SWAP_LONG), 2), " / ",
                              DoubleToString(SymbolInfoDouble(sym, SYMBOL_SWAP_SHORT), 2));

   // Try a few stop distances and see what 1% risk actually buys.
   double testStops[4] = {50, 100, 200, 400};
   for(int i = 0; i < 4; i++)
   {
      double sp   = testStops[i];
      double lots = CalcLotsForRisk(sym, riskMoney, sp);

      string note = "";
      if(sp < (double)stopsLevel)
         note = "  <-- inside broker stops level, would be REJECTED";
      else if(lots <= 0)
         note = "  <-- too small for this symbol";

      Print("Stop ", DoubleToString(sp, 0), " pts -> lots ", DoubleToString(lots, 2), note);
   }

   // Margin check on a candidate size
   double lots = CalcLotsForRisk(sym, riskMoney, 200);
   if(lots > 0)
   {
      double price  = SymbolInfoDouble(sym, SYMBOL_ASK);
      double margin = 0.0;
      if(OrderCalcMargin(ORDER_TYPE_BUY, sym, lots, price, margin))
      {
         double free = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
         Print("Required margin for ", DoubleToString(lots, 2), " lots: ",
               DoubleToString(margin, 2), "   free: ", DoubleToString(free, 2),
               "   -> ", (margin <= free ? "affordable" : "NOT affordable"));
      }
      else
         Print("OrderCalcMargin failed: ", GetLastError());
   }
}
如何執行
  1. 1. 從 MT5 開啟 MetaEditor(按 F4), 選擇這支程式該放的資料夾——ExpertsIndicators Scripts
  2. 2. 開新檔案、把這段程式貼上去,然後按 F7 編譯。Errors 分頁顯示的錯誤 都修掉。
  3. 3. 回到 MT5,把它拖到圖表上——如果是 EA,就打開 Strategy Tester

為什麼固定手數在不同商品上會失敗

0.10 手在每一種商品上代表的東西都不一樣。一個 CFD 指數的 0.10 手部位,與黃金或外匯貨幣對的 0.10 手有完全不同的虧損輪廓,因為合約規模與 tick value 不同。一個硬編成 0.10 手的 EA 在某些商品上過度承擔風險、在其他商品上風險不足,而交易者通常不知道是哪一種。

三個數字

  • SYMBOL_TRADE_TICK_SIZE——券商報價的最小價格增量。
  • SYMBOL_TRADE_TICK_VALUE——每一個 tick、每一手,以入金幣別計的盈虧金額。
  • SYMBOL_TRADE_CONTRACT_SIZE——每一手對應的標的物單位數。

注意 _Point 與 tick size 不總是相同。有些券商多報一位數,所以一個 point 可能是好幾個 tick。在乘上 tick value 之前,先把停損距離從 point 換算成 tick——這正是最多 sizing bug 跳過的那一步,而它會產生差了十倍的部位。

公式

每一手的風險等於「以 tick 表達的停損距離」乘以 tick value:

riskPerLot = (stopDistance / tickSize) * tickValue
lots       = riskMoney / riskPerLot

然後夾到商品的成交量下限、上限與級距——並且向下取整,永遠不要向上。

送出之前先檢查保證金

OrderCalcMargin 會告訴你這個部位需要多少保證金。拿它跟可用保證金比,不夠就退場。在槓桿之下,一個對風險來說規模正確的部位,仍然可能是你負擔不起的。

尊重 stops level 與 freeze level

SYMBOL_TRADE_STOPS_LEVEL 是 SL/TP 的最小距離;SYMBOL_TRADE_FREEZE_LEVEL 是你可以多靠近市價去修改一張單。如果你的風險導出停損落在 stops level 之內,你有兩個選擇:放大停損並縮小規模,或者跳過這筆交易。正確答案幾乎永遠是縮小規模——為了配合一個固定的手數而放大停損,會默默地把你的風險變成兩倍。

隔夜利息是成本,不是註腳

在抱過夜的 CFD 部位上,swap 會複利。一個忽略它的回測會高估多日持有的報酬。檢查 SYMBOL_SWAP_LONGSYMBOL_SWAP_SHORT,並在測試器裡放入真實的資金成本。

你剛完成了什麼

給 CFD 交易者的 MetaTrader 5 與 MQL5》的第 13 / 17 堂課。跑完範例或讀完這段之後, 把它勾起來,然後進下一堂課。