Back to MetaTrader 5 MQL5 for CFD Traders
Part 3

Position Sizing for CFD (The Lesson Most EAs Skip)

Lots are not risk. Convert tick value, contract size, and stop distance into a real position size.

Advanced13 min readBeginner → AdvancedLesson 13 / 17
Try it in MetaEditor
Risk-based sizing done correctly

Save as MQL5/Scripts/Lesson13Sizing.mq5. It converts tick size and tick value into a lot size for a 1% risk, validates it against the broker's limits, and checks margin.

//+------------------------------------------------------------------+
//| 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());
   }
}
To run it
  1. 1. Open MetaEditor from MT5 (press F4), and pick the folder this program belongs in — Experts, Indicators, or Scripts.
  2. 2. Create a new file, paste this over it, then press F7 to compile. Fix anything the Errors tab reports.
  3. 3. Back in MT5, drag it onto a chart — or open the Strategy Tester if it is an Expert Advisor.

Why fixed lots fail across symbols

0.10 lots means something different on every instrument. A 0.10 lot position in a CFD index has a completely different loss profile from 0.10 lots in gold or in a forex pair, because contract size and tick value differ. An EA hardcoded to 0.10 lots is over-risking on some symbols and under-risking on others, and the trader usually has no idea which.

The three numbers

  • SYMBOL_TRADE_TICK_SIZE — the smallest price increment the broker quotes.
  • SYMBOL_TRADE_TICK_VALUE — money gained or lost per tick, per one lot, in deposit currency.
  • SYMBOL_TRADE_CONTRACT_SIZE — units of the underlying per lot.

Note that _Point and tick size are not always the same. Some brokers quote an extra digit, so one point may be several ticks. Converting stop distance from points into ticks before multiplying by tick value is the step that most sizing bugs skip — and it produces positions that are wrong by a factor of ten.

The formula

Risk per lot equals the stop distance expressed in ticks, multiplied by tick value:

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

Then clamp to the symbol's volume minimum, maximum, and step — and round down, never up.

Check margin before you send

OrderCalcMargin tells you what the position will require. Compare it to free margin and stand down if it does not fit. With leverage, a position can be size-correct for risk and still be unaffordable.

Respect stops and freeze levels

SYMBOL_TRADE_STOPS_LEVEL is the minimum distance for SL/TP; SYMBOL_TRADE_FREEZE_LEVEL is how close to the market you may modify an order. If your risk-based stop lands inside the stops level you have a choice: widen the stop and cut the size, or skip the trade. The correct answer is almost always to cut the size — widening the stop to fit a fixed lot count silently doubles your risk.

Swap is a cost, not a footnote

On CFD positions held overnight, swap compounds. A backtest that ignores it will overstate returns on multi-day holds. Check SYMBOL_SWAP_LONG and SYMBOL_SWAP_SHORT and include realistic financing in the tester.

What you just did

Lesson 13 of 17 in MetaTrader 5 MQL5 for CFD Traders. When you have run the examples or read the section, tick it off and move to the next lesson.