Kwiz Computing Technologies Kwiz Computing Technologies
  • Home
  • Solutions
  • Environment
  • Technology
  • Kwiz Quants
  • Blog
  • About
  • Contact

Position Sizing for Forex: Kelly Criterion in R

quantitative-finance
systematic-trading
Implement full Kelly and fractional Kelly position sizing in R with bootstrap confidence intervals. Size for growth, not guesswork.
Author

Kwiz Computing Technologies

Published

April 23, 2026

Keywords

position sizing forex, Kelly criterion R, forex risk management, quantitative finance R, systematic trading

Traders spend months tuning entry signals and five minutes deciding how many lots to trade. That imbalance is a mistake, because position sizing determines the shape of your equity curve far more than any entry condition does.

This is not opinion. It follows directly from the mathematics of compound growth. Two traders running the same signals but different sizing rules will produce completely different account trajectories over 500 trades. This article implements the Kelly criterion in R so you can find the sizing that maximises long-run growth from your actual backtest data.

Why Sizing Dominates Entry Signals

Consider a coin-flip strategy: 55% win rate, pays 1:1. Trade it at 50% of capital per flip and you will almost certainly go broke, because a short losing streak wipes you out before the edge compounds. Trade it at 1% and you survive but grow slowly. Trade it at the Kelly-optimal 10% and geometric growth is maximised.

Mathematically, the long-run compound growth rate of a strategy is:

\[g = \ln(1 + f \cdot b) \cdot p + \ln(1 - f) \cdot (1 - p)\]

where f is the fraction of capital risked, b is the win-to-loss ratio, and p is the win rate. This expression has a maximum at a specific f. Above that maximum, growth falls, and well above it, ruin becomes certain. That maximum is the Kelly fraction.

A systematic forex trader in Nairobi running a 52% win rate breakout strategy on EURUSD has an edge. Whether that edge translates into account growth depends almost entirely on how they size positions.

The Kelly Criterion: Formula and Derivation

John Kelly derived his formula in 1956 to solve optimal bet sizing for a gambler with a noisy signal. For a binary outcome game (win b units or lose 1 unit per bet), the Kelly fraction is:

\[f^* = \frac{p \cdot b - (1 - p)}{b} = p - \frac{q}{b}\]

where p is win probability and q = 1 - p. The formula tells you the fraction of current capital to risk on each trade. For a strategy with 55% wins and average payoff ratio 1.5:

\[f^* = 0.55 - \frac{0.45}{1.5} = 0.55 - 0.30 = 0.25\]

Risk 25% of equity per trade. That is the full Kelly recommendation for those parameters, and it is almost certainly too aggressive for a live forex account. We will get to why shortly.

Estimating Parameters and Computing Kelly in R

Before sizing, you need reliable estimates of p and b from real trade data. The following R code reads an MT5-style trade log and computes both parameters.

library(dplyr)
library(readr)

# MT5 exports a semicolon-delimited file; adjust sep as needed
trades <- read_delim("mt5_history.csv", delim = ";", col_types = cols()) |>
  rename_with(tolower) |>
  filter(type %in% c("buy", "sell")) |>    # exclude deposits/withdrawals
  mutate(profit = as.numeric(profit))

# Separate wins from losses
wins  <- trades |> filter(profit > 0)
losses <- trades |> filter(profit < 0)

win_rate   <- nrow(wins) / nrow(trades)
avg_win    <- mean(wins$profit)
avg_loss   <- abs(mean(losses$profit))
payoff_ratio <- avg_win / avg_loss

cat(sprintf(
  "Trades: %d | Win rate: %.1f%% | Avg win: %.2f | Avg loss: %.2f | Payoff: %.2f\n",
  nrow(trades), win_rate * 100, avg_win, avg_loss, payoff_ratio
))

For real East African forex traders operating on ECN brokers through Nairobi-based prop desks, the average trade log contains 200 to 800 trades per year. That is enough data to estimate win rate with reasonable precision, but not enough to treat the estimate as exact. We return to this limitation in the next section.

From Parameters to Lot Size

With p and b in hand, the Kelly fraction is a single line of arithmetic:

kelly_fraction <- function(win_rate, payoff_ratio) {
  q <- 1 - win_rate
  (win_rate * payoff_ratio - q) / payoff_ratio
}

f_star <- kelly_fraction(win_rate, payoff_ratio)
cat(sprintf("Full Kelly fraction: %.4f (%.1f%% of equity per trade)\n",
            f_star, f_star * 100))

To convert that fraction into an MT5 lot size for a given account balance and instrument:

lot_size_from_kelly <- function(balance_usd, kelly_f, pip_value, stop_loss_pips) {
  # kelly_f is fraction of equity to risk
  # pip_value in USD per standard lot
  risk_usd  <- balance_usd * kelly_f
  lots      <- risk_usd / (stop_loss_pips * pip_value)
  round(lots, 2)
}

# Example: $10,000 account, EURUSD, 10 pip SL, pip value = $10/lot
lot_size_from_kelly(
  balance_usd    = 10000,
  kelly_f        = f_star,
  pip_value      = 10,
  stop_loss_pips = 10
)

This gives a concrete lot size you can drop into an Expert Advisor or a manual order.

Why Full Kelly Is Almost Always Too Aggressive

The Kelly fraction is extremely sensitive to the input estimates. A win rate of 55% yields a Kelly fraction well above zero. A win rate of 49%, estimated from a different 300-trade sample of the same strategy, produces a negative Kelly fraction: do not trade. That gap between estimates is entirely plausible due to sampling noise.

This is the core problem. Full Kelly sized to your point estimate of p and b is almost always too aggressive, because your estimates contain error. The resulting over-sizing produces severe drawdowns that compound faster than your edge can recover them.

Bootstrap confidence intervals make this concrete. You can resample your trade history to see how much the Kelly fraction bounces around:

library(rsample)
library(purrr)

bootstrap_kelly <- function(trades, n_boot = 2000) {
  boot_samples <- bootstraps(trades, times = n_boot)

  map_dbl(boot_samples$splits, function(split) {
    d      <- analysis(split)
    wins   <- d |> filter(profit > 0)
    losses <- d |> filter(profit < 0)

    if (nrow(wins) == 0 || nrow(losses) == 0) return(NA_real_)

    p_b <- nrow(wins) / nrow(d)
    b_b <- mean(wins$profit) / abs(mean(losses$profit))
    kelly_fraction(p_b, b_b)
  }) |>
    na.omit()
}

boot_kelly <- bootstrap_kelly(trades)

cat(sprintf(
  "Kelly fraction: point est = %.3f | 5th pct = %.3f | 95th pct = %.3f\n",
  f_star,
  quantile(boot_kelly, 0.05),
  quantile(boot_kelly, 0.95)
))

# Plot the bootstrap distribution
hist(boot_kelly,
     breaks = 50,
     main   = "Bootstrap Distribution of Kelly Fraction",
     xlab   = "Kelly f*",
     col    = "#4E79A7",
     border = "white")
abline(v = f_star, col = "red", lwd = 2, lty = 2)
abline(v = quantile(boot_kelly, c(0.05, 0.95)), col = "orange", lwd = 1.5, lty = 3)

When a strategy has 300 trades, the 90% bootstrap interval for the Kelly fraction typically spans 15 percentage points or more. Sizing to the upper end of that range is ruin territory. This is why practitioners universally apply a fractional Kelly.

See the related discussion on overfitting and estimation bias in our post on combinatorial purged cross-validation, which applies the same resampling logic to strategy selection.

Half-Kelly and Fractional Kelly in Practice

The solution is to trade a fixed fraction of the Kelly recommendation. Half-Kelly (f = 0.5 * f*) is the most common choice, and it has attractive theoretical properties: it retains roughly 75% of the full Kelly growth rate while halving the variance of outcomes and reducing the severity of drawdowns substantially.

compare_sizing_methods <- function(trades, account_size = 10000,
                                   pip_value = 10, stop_loss_pips = 10) {
  wins   <- trades |> filter(profit > 0)
  losses <- trades |> filter(profit < 0)

  p_est <- nrow(wins) / nrow(trades)
  b_est <- mean(wins$profit) / abs(mean(losses$profit))
  f_k   <- kelly_fraction(p_est, b_est)

  methods <- tibble(
    method       = c("Full Kelly", "Half Kelly", "Fixed 2%", "Fixed 1%"),
    fraction     = c(f_k, f_k / 2, 0.02, 0.01),
    lots = map_dbl(fraction, \(f)
      lot_size_from_kelly(account_size, f, pip_value, stop_loss_pips))
  )

  methods
}

sizing_table <- compare_sizing_methods(trades)
print(sizing_table)

The fixed 1% and 2% rules are simple and widely used, including by many prop firms and retail brokers operating in East Africa. They work, but they are arbitrary. If your strategy has a Kelly fraction of 4%, the 1% rule is overly conservative and leaves growth on the table. If your Kelly fraction is 1%, the 2% rule is over-leveraged.

Fractional Kelly anchors your sizing to your actual edge rather than an arbitrary number.

Simulating Growth Trajectories

Simulating forward shows the practical difference between methods. The following code runs a Monte Carlo on your estimated trade distribution:

simulate_equity <- function(trades, fraction, n_trades = 500,
                            n_sims = 200, start_equity = 10000) {
  # Normalise profits to fraction-of-equity units
  wins_pct   <- trades |> filter(profit > 0) |> pull(profit) / start_equity
  losses_pct <- trades |> filter(profit < 0) |> pull(profit) / start_equity

  replicate(n_sims, {
    equity <- start_equity
    for (i in seq_len(n_trades)) {
      if (runif(1) < win_rate) {
        r <- sample(wins_pct, 1)
      } else {
        r <- sample(losses_pct, 1)
      }
      equity <- equity * (1 + fraction * sign(r) * abs(r) /
                            (abs(trades$profit |> mean())))
      if (equity < 0) { equity <- 0; break }
    }
    equity
  })
}

set.seed(42)
results <- list(
  full_kelly  = simulate_equity(trades, f_star),
  half_kelly  = simulate_equity(trades, f_star / 2),
  fixed_2pct  = simulate_equity(trades, 0.02),
  fixed_1pct  = simulate_equity(trades, 0.01)
)

# Median terminal equity per method
sapply(results, median)

Across typical East African retail forex strategy parameters (win rates 50-58%, payoff ratios 1.2-1.8), half-Kelly consistently outperforms fixed rules on both median growth and ruin rate over 500-trade horizons. Full Kelly produces the highest median but with dramatically fatter left tails.

This connects to the broader framework for evaluating whether your strategy has genuine edge in the first place, covered in our Deflated Sharpe Ratio post. Position sizing on a false edge accelerates ruin rather than growth.

Live MT5 Integration

For traders running Expert Advisors on MetaTrader 5, the Kelly lot calculation needs to happen at order submission time, using current account equity rather than initial capital. A clean pattern:

# Run this from a Plumber API endpoint called by the EA before order submission
# See our MT5-R bridge architecture in the systematic trading post

current_equity <- 12500  # fetched from MT5 account info
kelly_lots <- lot_size_from_kelly(
  balance_usd    = current_equity,
  kelly_f        = f_star / 2,    # half-Kelly
  pip_value      = 10,
  stop_loss_pips = 15             # from strategy signal
)

# Return to EA
list(recommended_lots = kelly_lots, method = "half-kelly", fraction = f_star / 2)

This pattern updates position sizing dynamically as account equity changes, which is exactly what Kelly requires. A growing account takes larger positions; a shrinking account cuts back automatically. For a deeper look at building R-based trading infrastructure that connects to MT5, see Systematic Trading Infrastructure in R.

The Bottom Line

Kelly sizing is not a silver bullet. It requires honest estimates of your edge, bootstrap intervals to understand estimation uncertainty, and the discipline to apply fractional Kelly rather than the full formula. Applied properly, it gives you a principled answer to the question that matters most in long-run trading: not which direction to trade, but how much.

The traders on Nairobi’s retail forex desks who run consistent edge lose that edge to poor sizing more often than to poor signals. If you have a backtested strategy with documented win rate and payoff data, run the numbers. A 15-minute R script will tell you whether your current sizing is leaving growth on the table or pushing you toward ruin.

What does your bootstrap Kelly interval look like? If you share your trade log, the Kwiz Quants team can run a full sizing analysis as part of our strategy evaluation service.

© 2026 Kwiz Computing Technologies. All rights reserved.
Data Science & Technology | Environmental Analytics | Quantitative Finance

 

Built with Quarto