Lot size calculator formula: pip value and sizing in MT5

8 min readMetaKit

You want to risk 1% of a $10,000 account on a EURUSD trade with a 25-pip stop. The answer is 0.40 lots. Most people get there by plugging numbers into a website, and then have no idea what to do when the instrument is gold, the account is in euros, or the broker's minimum lot is bigger than the answer.

The lot size calculator formula is three lines of arithmetic. The hard part is the inputs, because every one of them comes from the broker.

A lot is a contract size, not a number

One standard lot is 100,000 units of the base currency. One lot of EURUSD is 100,000 euros. A mini lot is 0.10 (10,000 units), a micro lot is 0.01 (1,000 units), and MT5 expresses all of them as decimal fractions of a standard lot.

That 100,000 is not a law of nature. It's the symbol's contract_size, and it's 100,000 on forex almost everywhere, but 100 troy ounces on most gold symbols, 5,000 on silver, and often 1 on indices and share CFDs. The symbol suffix post covers how much this varies between brokers. For sizing, the point is simpler: never write 100000 in your code. Read it.

Pip vs point

A point is the smallest price increment the broker quotes: 10 ** -digits. A pip is the traditional unit traders talk in: 0.0001 on most pairs, 0.01 on yen pairs.

On a five-digit broker, EURUSD is quoted as 1.08453, one point is 0.00001, and one pip is ten points. On a four-digit broker they're the same thing. This is why stop_pips * 0.0001 is wrong on USDJPY (a pip is 0.01 there) and stop_points * point is right everywhere, provided you compute the point from digits.

Gold is where the word "pip" falls apart. On a two-digit gold quote (2031.45) some people call 0.01 a pip, others call 0.10 a pip, and a few mean a full dollar. Don't use the word. Use tick_size, which the broker defines and which never argues back.

Pip value, three ways

Pip value is money per pip per lot, in the account currency. The general formula is contract_size × pip_size, which gives a value in the quote currency, then converted to the account currency if they differ.

Quote-USD pair on a USD account (EURUSD). 100,000 × 0.0001 = $10 per pip per lot. No conversion. This is the number everyone memorises, and it's only true for pairs that end in the account currency.

USD-base pair (USDJPY at 150.00). 100,000 × 0.01 = 1,000 JPY per pip per lot. Convert to USD by dividing by the rate: 1,000 / 150.00 = $6.67. Move the rate to 140.00 and it becomes $7.14. Pip value on these pairs changes with price, which is why yesterday's number is wrong today.

Cross pair (EURGBP on a USD account, GBPUSD at 1.2700). 100,000 × 0.0001 = 10 GBP per pip per lot. Convert through GBPUSD: 10 × 1.2700 = $12.70. You need a second quote to size the first trade, and if the account is in EUR you need a different second quote.

Gold (XAUUSD, 100 oz contract, USD account). One point of 0.01 is 100 × 0.01 = $1 per lot. A one-dollar move is $100 per lot. On a three-digit gold feed the point is $0.10, but a one-dollar move is still $100, because contract size didn't change. Size gold in dollars of price movement, not pips, and the digits stop mattering.

There is a shortcut that handles all four cases without you doing the currency conversion: MT5 publishes tick_value for every symbol, the money one tick_size move is worth per lot, already in the account currency. Pip value is then tick_value × (pip_size / tick_size). The broker did the cross-rate lookup for you. Read it fresh, since it moves with price on non-USD pairs.

Risk-based position sizing, with numbers

The formula:

lots = risk_money / (stop_distance_in_pips × pip_value_per_lot)

Example 1. $10,000 account, 1% risk, EURUSD, 25-pip stop. Risk money is $100. Pip value is $10. Lots = 100 / (25 × 10) = 0.40. Check it: 0.40 lots × 25 pips × $10 = $100. Good.

Example 2. $5,000 account, 2% risk, USDJPY at 150.00, 40-pip stop. Risk money is $100. Pip value is $6.67. Lots = 100 / (40 × 6.67) = 0.3748. The broker's lot_step is 0.01, so round down to 0.37. Actual risk is 0.37 × 40 × 6.67 = $98.72. Rounding up to 0.38 would risk $101.38, which is over budget, and "over budget by a dollar" becomes "over budget by 8%" once the account is smaller and the step is coarser.

Example 3. $20,000 account, 0.5% risk, XAUUSD, stop $8 away. Risk money is $100. At $1 per 0.01 point per lot, an $8 stop is 800 points, so risk per lot is $800. Lots = 100 / 800 = 0.125. Round down to the 0.01 step: 0.12. Actual risk $96.

And the case the website calculators skip: $300 account, 1% risk, EURUSD, 30-pip stop. Risk money $3. Lots = 3 / (30 × 10) = 0.01. Fine. Make the stop 40 pips and the answer is 0.0075, which is below the broker's min_lot of 0.01. There is no lot size that satisfies the risk rule. Either skip the trade or knowingly take 1.33% risk. The formula can't decide that for you, but your code has to notice, rather than silently rounding 0.0075 up to 0.01.

Rounding is part of the formula

Every broker publishes three volume constraints per symbol: min_lot, max_lot, and lot_step. Orders that don't sit on the grid are rejected outright with "invalid volume", and the grid is not always 0.01. Index CFDs with a step of 0.1 or a minimum of 1.0 are common.

Always floor to the step, then clamp. Floor because rounding to nearest can push you over the risk budget; clamp because a 0.005 result on a 0.01 minimum means "don't", not "0.01". Do the floor in integer steps to dodge floating point: floor(raw / step) * step, then round to a sensible number of decimals so 0.37000000000000005 doesn't reach the broker.

The same maths runs inside a copier

A trade copier is a position sizer that runs on someone else's trade. The three sizing modes in a MetaKit copier map directly onto what you just did by hand:

  • multiplier: follower lots = master lots × lot_value. The master already did the risk calculation; you're scaling their answer. Use this when both accounts trade the same instruments on the same contract sizes.
  • fixed: follower lots = lot_value, always. Predictable and mostly wrong, since the master's 0.10 on a 10-pip stop and their 0.10 on a 100-pip stop carry very different risk.
  • proportional (balance-ratio): follower lots = master lots × (follower balance / master balance). A $10k follower copies a $100k master at 0.1×. This is the mode that feels like risk parity, and it's the one that goes wrong in interesting ways.

Balance-ratio breaks when the two accounts aren't comparable. If the master is in USD and the follower in EUR, the ratio compares raw balance numbers in different currencies and is off by the exchange rate. If the follower is on 1:30 leverage and the master on 1:500, the scaled lots can be perfectly proportional and still exceed the follower's free margin, so the order is rejected on "not enough money" while the master sails on. And if the two brokers disagree on contract size, proportional lots are proportional amounts of different things. The ratio is a good starting point and a bad final answer; filters.max_lot exists to put a ceiling under the surprises.

A function that does it from symbol info

Everything above collapses into one function once the inputs come from the broker. This pulls the symbol spec through MetaKit's GET /v1/accounts/{id}/symbols/{symbol} and sizes from tick_size, tick_value, and the volume constraints. The stop is in price units, not pips, so the same code works on EURUSD (0.0025 for 25 pips), USDJPY (0.40 for 40 pips), and gold (8.0 for eight dollars).

import math
import os
import requests
 
BASE = "https://api.metakit.cloud"
HEADERS = {"Authorization": f"Bearer {os.environ['METAKIT_KEY']}"}
 
 
def symbol_spec(account_id: int, symbol: str) -> dict:
    res = requests.get(
        f"{BASE}/v1/accounts/{account_id}/symbols/{symbol}", headers=HEADERS
    )
    res.raise_for_status()
    return res.json()
 
 
def lots_for_risk(spec: dict, risk_money: float, stop_distance: float) -> float:
    """Return a broker-valid lot size, or 0.0 if the risk can't be met.
 
    spec: the symbol spec from the API (tick_size, tick_value, min_lot, ...).
    risk_money: amount to risk, in the account currency.
    stop_distance: stop distance in price units (0.0025 = 25 pips on EURUSD).
    """
    ticks_in_stop = stop_distance / spec["tick_size"]
    risk_per_lot = ticks_in_stop * spec["tick_value"]  # account currency
    raw = risk_money / risk_per_lot
 
    step = spec["lot_step"]
    lots = math.floor(raw / step + 1e-9) * step
    lots = round(lots, 8)
 
    if lots < spec["min_lot"]:
        return 0.0
    return min(lots, spec["max_lot"])
 
 
account = requests.get(f"{BASE}/v1/accounts/2", headers=HEADERS).json()
risk = account["balance"] * 0.01
 
for symbol, stop in [("EURUSD", 0.0025), ("USDJPY", 0.40), ("XAUUSD", 8.0)]:
    spec = symbol_spec(2, symbol)
    print(symbol, lots_for_risk(spec, risk, stop))

The account has to be connected before the spec endpoint returns anything (it's read live from the broker, so you get today's tick_value, not a cached one). Note also that tick_value on a non-USD pair drifts with price during the day; sizing a trade in the morning with a spec you fetched last night is the same category of bug as hardcoding $10.

Full field list for the symbol spec and the copier sizing options in the API reference.

If you take one thing from this: the formula is trivial, the constants are not constants. Pip value moves with price, contract size moves with broker, and the lot grid moves with symbol. Fetch all three, every time, and the 0.40 will take care of itself.