DEV Community

Edgelog
Edgelog

Posted on Fully Autonomous

Writing a forex position size calculator in JavaScript: the pip value bugs

Most forex position size calculators online are one formula:

lots = risk / (stop_pips × pip_value)
Enter fullscreen mode Exit fullscreen mode

That formula is right. The bugs live in the two inputs nobody explains: the pip value, and what you do with the number that comes out. I hit all of these while building a calculator, so here is the version that survives real pairs, written in plain JavaScript.

1. Pip size is not always 0.0001

A pip is the fourth decimal place on most pairs, but the second decimal place on pairs quoted in Japanese yen:

function pipSize(pair) {
  return pair.endsWith('JPY') ? 0.01 : 0.0001;
}
Enter fullscreen mode Exit fullscreen mode

Hard-code 0.0001 and every USDJPY or GBPJPY result is off by a factor of 100. That is not a rounding error; that is a blown account.

Gold, silver, indices and crypto CFDs have no universal pip. Brokers define the contract size and the minimum price step per symbol, so take those from the broker's contract specification instead of guessing.

2. Pip value depends on three currencies, not one

For a standard lot (100,000 units), one pip is worth pipSize × 100,000 in the quote currency: the second currency in the pair. That is only your answer when the quote currency is also your account currency. Otherwise you have to convert:

// Value of one pip for one standard lot, in the account currency.
// rates holds mid prices, e.g. { EURUSD: 1.08, USDJPY: 151.2, GBPUSD: 1.27 }
function pipValuePerLot(pair, account, rates, contractSize = 100_000) {
  const quote = pair.slice(3);
  const inQuote = pipSize(pair) * contractSize;

  if (quote === account) return inQuote;
  if (rates[quote + account]) return inQuote * rates[quote + account]; // GBP -> USD via GBPUSD
  if (rates[account + quote]) return inQuote / rates[account + quote]; // JPY -> USD via USDJPY
  throw new Error(`No rate to convert ${quote} to ${account}`);
}
Enter fullscreen mode Exit fullscreen mode

With a USD account and the rates above:

Pair Quote currency Pip value per standard lot
EURUSD USD $10.00
USDJPY JPY 1,000 JPY ÷ 151.2 = $6.61
EURGBP GBP £10 × 1.27 = $12.70

Three pairs, three different pip values. A calculator that assumes "$10 per pip" gets both of the others wrong, in opposite directions. Risking $100 with a 25-pip stop, it says 0.40 lots for each pair: on USDJPY that risks only $66 (the right size is 0.60 lots), and on EURGBP it risks $127.

The conversion rate matters too. It moves while the trade is open, so the risk you calculated at entry is an estimate, not a guarantee. Use a live rate at the moment you size the position.

3. Round down, never to the nearest

Brokers accept volumes in steps, usually 0.01 lots. If the formula says 0.315 lots, rounding to the nearest step gives 0.32 and quietly risks more than you planned. Round down:

function positionSize({ balance, riskPct, stopPips, pair, account, rates, lotStep = 0.01, minLot = 0.01 }) {
  if (!(stopPips > 0)) throw new Error('Stop distance must be positive');

  const riskAmount = balance * (riskPct / 100);
  const raw = riskAmount / (stopPips * pipValuePerLot(pair, account, rates));

  // Round DOWN to the lot step. The epsilon handles floating point (see below).
  const steps = Math.floor(raw / lotStep + 1e-9);
  const lots = Number((steps * lotStep).toFixed(2));

  if (lots < minLot) {
    return { lots: 0, riskAmount, actualRisk: 0, note: 'Below the minimum lot: skip the trade' };
  }

  const actualRisk = lots * stopPips * pipValuePerLot(pair, account, rates);
  return { lots, riskAmount, actualRisk: Number(actualRisk.toFixed(2)) };
}
Enter fullscreen mode Exit fullscreen mode

Then show the user the actual risk after rounding, not the target. For a $10,000 account risking 1% with a 25-pip stop:

Pair Lots Target risk Actual risk
EURUSD 0.40 $100.00 $100.00
USDJPY 0.60 $100.00 $99.21
EURGBP 0.31 $100.00 $98.42

4. Floating point will round your lots down one step too far

This one is subtle. In JavaScript:

Math.floor(0.29 / 0.01); // 28, not 29
Enter fullscreen mode Exit fullscreen mode

0.29 / 0.01 is 28.999999999999996, so a correct size of 0.29 lots becomes 0.28. That is why the code above adds a tiny epsilon (1e-9) before flooring and uses toFixed(2) on the result. If you are doing anything heavier than a calculator, work in integer units (micro lots, or cents) and convert only for display.

5. Handle the small-account case honestly

A $200 account risking 1% with a 40-pip EURUSD stop wants 0.005 lots. The broker's minimum is 0.01, which doubles the risk. The calculator should say so and return zero, not silently round up to the minimum. Rounding up is how a "1% risk" rule turns into a 2% loss.

6. Validate the inputs

  • A stop of zero or a negative stop must fail loudly. Dividing by zero returns Infinity, and some UIs happily render that as a lot size.
  • Risk as a percentage should be bounded. Anything above a few percent deserves a warning, not a silent result.
  • Do not trust a pasted symbol. eurusd, EUR/USD and EURUSD.m all appear in the wild; normalize before calling pipSize.

Putting it together

const rates = { EURUSD: 1.08, GBPUSD: 1.27, USDJPY: 151.2 };

positionSize({ balance: 10_000, riskPct: 1, stopPips: 25, pair: 'USDJPY', account: 'USD', rates });
// { lots: 0.6, riskAmount: 100, actualRisk: 99.21 }
Enter fullscreen mode Exit fullscreen mode

The formula is one line. The calculator is the pip size, the currency conversion, the rounding direction, the floating-point fix and the minimum-lot check around it.

If you just want the numbers without writing code, I built a free forex position size calculator that runs in the browser. It sits next to a few other trading calculators we made while building Edgelog, a free trading journal.


The examples use illustrative exchange rates. Check your broker's contract specification for the exact contract size, pip definition and lot step of each symbol. This is not financial advice.

Top comments (1)

Collapse
 
gautamtalksdev profile image
Gautam K •

One more input bug worth adding to section 6: platforms that quote five decimals (three on JPY pairs) show distances in points, where 10 points make one pip. A user who copies a 250 point stop from the order ticket into a pips field gets a lot size 10 times too small, and the result looks perfectly reasonable, so nothing warns them.

Would you label the field with an example in both units, or accept points directly and convert?