Edgoro
Strategy

What is a good profit factor? What the number means and how many trades it needs

Profit factor explained with real math: its link to win rate and expectancy, how far luck moves it, what costs do, and code for TradingView and MT5.

Profit factor is the first number most traders look at in a backtest report. TradingView's Strategy Tester shows it, and so does the MetaTrader 5 tester report. It's simple: money won divided by money lost. The question everyone asks next is what value counts as good, and most answers give a single number.

A single number doesn't work, for two reasons we can measure: profit factor moves a lot by chance over a few dozen or a few hundred trades, and trading costs take a big bite out of it. This guide shows both with simulations and gives a checklist that holds up better than a threshold.

What profit factor is

profit factor = gross profit ÷ gross loss

Gross profit is the sum of all winning trades, gross loss the sum of all losing trades as a positive number. Above 1 the strategy made money over the test; below 1 it lost.

With a win rate p, an average win W and an average loss L, it is:

profit factor = (p × W) ÷ ((1 − p) × L)
expectancy    = p × W − (1 − p) × L = (1 − p) × L × (profit factor − 1)

So the same profit factor can come from very different strategies:

Win rateAverage winAverage lossProfit factorExpectancy per trade
30%3R1R1.29+0.20R
50%1.5R1R1.50+0.25R
60%1R1R1.50+0.20R
70%0.6R1R1.40+0.12R

R is the amount risked per trade. The 30% strategy has the lowest profit factor and the same expectancy as the 60% one. Profit factor alone doesn't tell you how much a trade earns; expectancy does. The expectancy calculator gives both from your win rate and payoff.

How much profit factor moves by chance

Take a strategy with a real edge: 45% winners, wins of 1.8R, losses of 1R, so a true profit factor of 1.47. We simulated it 20,000 times for each test length, and recorded the profit factor each test would have shown:

Trades in the test5% of tests belowMedian5% of tests aboveTests showing a loss (PF < 1)
300.771.382.7013.4%
500.931.412.297.4%
1001.061.472.032.6%
3001.221.471.78under 0.1%
1,0001.331.471.63under 0.1%

Over 50 trades, the same strategy can show anything from 0.93 to 2.29. Now take a strategy with no edge at all (50% winners at 1:1, true profit factor 1.0) and count how often luck alone shows a profit factor of 1.5 or more:

TradesPF of 1.5 or morePF of 2 or more
2024.6%5.8%
3018.0%4.5%
5010.3%0.9%
1003.1%under 0.1%
2000.3%under 0.1%

And if you test 20 such no-edge variants on 100 trades each and keep the best, its profit factor has a median of 1.44 (5% to 95% of the time: 1.22 to 1.78). An optimizer that tries 20 settings will usually find a profit factor around 1.4 in pure noise.

What costs do to it

Costs take the same amount from every trade, which lowers winners and deepens losers at once. For the 45% / 1.8R strategy, with the cost per trade expressed in R:

Cost per tradeProfit factorExpectancy
01.47+0.26R
0.05R1.36+0.21R
0.10R1.26+0.16R
0.20R1.09+0.06R

A backtest without commission, spread and slippage overstates the profit factor; on short timeframes, where the stop is small, the cost in R is large. The trading fee calculator converts your fees into R.

So what is a good profit factor?

There's no threshold that works for every strategy. A profit factor is good when it passes these checks:

  1. It's above 1 after costs, with commission, spread and slippage set in the backtest.
  2. The test has enough trades that luck can't explain it. The backtest sample size calculator tells you how many, from your win rate and payoff; for the 45% / 1.8R strategy it's about 111 trades at 95% confidence.
  3. You count every version you tried. The best of 20 tries needs a lot more evidence than a single test.
  4. It holds on data the optimization never saw. Walk-forward analysis is the standard way.
  5. It doesn't rest on a few trades. Remove the single best trade and recompute; if the profit factor collapses, so does the strategy.

A very high profit factor on few trades is a reason for suspicion, not celebration: it's what overfitting and luck look like.

Measure the noise in TradingView

This strategy prints the profit factor next to the number of closed trades, the average trade and the lower 95% bound of the average trade. When the lower bound is above zero, the average trade is clear of noise at that confidence. The EMA crossover is only there to produce trades.

//@version=6
strategy("Profit factor and its noise (example)", overlay = true, initial_capital = 10000, default_qty_type = strategy.percent_of_equity, default_qty_value = 10, commission_type = strategy.commission.percent, commission_value = 0.05, slippage = 2)

fastLen = input.int(20, "Fast EMA", minval = 1)
slowLen = input.int(50, "Slow EMA", minval = 2)

fast      = ta.ema(close, fastLen)
slow      = ta.ema(close, slowLen)
crossUp   = ta.crossover(fast, slow)
crossDown = ta.crossunder(fast, slow)

if crossUp
    strategy.entry("Long", strategy.long)
if crossDown
    strategy.close("Long")

plot(fast, "Fast EMA", color = color.teal)
plot(slow, "Slow EMA", color = color.orange)

// Profit factor, average trade and the lower 95% bound of the average trade, on the last bar.
var table box = table.new(position.top_right, 2, 4, bgcolor = color.new(color.gray, 80))
if barstate.islast
    n     = strategy.closedtrades
    sum   = 0.0
    sumSq = 0.0
    if n > 0
        for i = 0 to n - 1
            p = strategy.closedtrades.profit(i)
            sum   += p
            sumSq += p * p
    mean  = n > 0 ? sum / n : na
    sd    = n > 1 ? math.sqrt(math.max(0.0, (sumSq - n * mean * mean) / (n - 1))) : na
    lower = n > 1 ? mean - 1.96 * sd / math.sqrt(n) : na
    pf    = strategy.grossloss > 0 ? strategy.grossprofit / strategy.grossloss : na
    table.cell(box, 0, 0, "Closed trades")
    table.cell(box, 1, 0, str.tostring(n))
    table.cell(box, 0, 1, "Profit factor")
    table.cell(box, 1, 1, str.tostring(pf, "#.##"))
    table.cell(box, 0, 2, "Average trade")
    table.cell(box, 1, 2, str.tostring(mean, "#.##"))
    table.cell(box, 0, 3, "95% lower bound")
    table.cell(box, 1, 3, str.tostring(lower, "#.##"), text_color = lower > 0 ? color.green : color.red)

The n > 0 check keeps the loop from running before the first trade has closed, and the table only updates on the last bar, so the loop runs once rather than on every bar.

Rank MT5 optimizations by the lower bound

In the MT5 optimizer, ranking settings by profit factor rewards the luckiest one. This OnTester() returns the lower 95% bound of the average trade instead, so a setting needs both a good average and enough trades to rank high. Choose "Custom max" as the optimization criterion. It compiled in MetaEditor with 0 errors and 0 warnings.

// OnTester() that ranks settings by the lower 95% bound of the average trade, so the optimizer prefers an edge that
// is large compared with its noise. Paste OnTester() into your own EA; OnTick() below stands for your EA's code.
input int MinTrades = 100;   // fewer closed trades than this scores 0

void OnTick()
{
   // Your EA's trading code.
}

double OnTester()
{
   if(!HistorySelect(0, TimeCurrent()))
      return 0.0;
   double sum = 0.0, sumSq = 0.0, grossProfit = 0.0, grossLoss = 0.0;
   int n = 0;
   for(int i = 0; i < HistoryDealsTotal(); i++)
   {
      ulong deal = HistoryDealGetTicket(i);
      if(deal == 0 || HistoryDealGetInteger(deal, DEAL_ENTRY) != DEAL_ENTRY_OUT)
         continue;
      double r = HistoryDealGetDouble(deal, DEAL_PROFIT) + HistoryDealGetDouble(deal, DEAL_SWAP) + HistoryDealGetDouble(deal, DEAL_COMMISSION);
      sum += r;
      sumSq += r * r;
      if(r > 0.0)
         grossProfit += r;
      else
         grossLoss -= r;
      n++;
   }
   if(n < MinTrades || n < 2)
      return 0.0;
   double mean = sum / n;
   double sd = MathSqrt(MathMax(0.0, (sumSq - n * mean * mean) / (n - 1)));
   double lower = mean - 1.96 * sd / MathSqrt(n);
   PrintFormat("Trades %d, profit factor %.2f, average trade %.2f, 95%% lower bound %.2f", n, grossLoss > 0.0 ? grossProfit / grossLoss : 0.0, mean, lower);
   return lower;   // negative: the average trade is not clearly above zero
}

For the plain profit factor criterion and the tester's modelling modes, see MT5 Strategy Tester modelling modes.

A range for any list of trades, in Python

With your own trade results (exported from TradingView or MT5), a bootstrap gives a range for the profit factor without any assumption about the distribution: resample the trades many times and look at the spread.

import random


def profit_factor(results):
    gross_profit = sum(r for r in results if r > 0)
    gross_loss = -sum(r for r in results if r < 0)
    return gross_profit / gross_loss if gross_loss else float("inf")


def pf_range(results, runs=10_000, seed=1):
    """5th and 95th percentiles of the profit factor over bootstrap resamples of the trades."""
    rng = random.Random(seed)
    pfs = sorted(profit_factor(rng.choices(results, k=len(results))) for _ in range(runs))
    return pfs[int(0.05 * runs)], pfs[int(0.95 * runs)]


trades = [1.8] * 45 + [-1.0] * 55  # 100 trades: 45 wins of 1.8R, 55 losses of 1R
print(round(profit_factor(trades), 2), [round(x, 2) for x in pf_range(trades)])

It prints 1.47 [1.06, 2.03]: the same range as the simulation above for 100 trades.

How the numbers were made

The profit factor tables come from 20,000 simulated tests per row (numpy, seed 20261111), drawing each trade as a win or a loss at the stated win rate; the best-of-20 figure from 5,000 runs of 20 strategies. The cost table and the identities are plain arithmetic from the formulas above.

Test it with Edgoro

Describe your strategy in plain English and the Pine Script generator writes it as a TradingView strategy with commission and slippage set, ready for the Strategy Tester. The Backtest Lab tests crypto strategies on real exchange data with fees included.

This article is for education. A good profit factor in a backtest doesn't guarantee future results; markets change and costs vary. Test on a demo account before risking money.

Frequently asked questions

What is a good profit factor?

One above 1 after costs, on enough trades that luck can't explain it, and that holds on data the optimization never saw. There is no universal threshold: a no-edge strategy shows a profit factor of 1.5 or more in 10% of 50-trade tests, while a real 1.47 strategy can show 0.93 over 50 trades.

How is profit factor calculated?

Gross profit divided by gross loss: the sum of winning trades over the sum of losing trades (as a positive number). With a win rate p, average win W and average loss L, it equals p × W ÷ ((1 − p) × L).

Is a profit factor of 1.5 good?

It depends on the number of trades and costs. Over 100 trades with costs included, a profit factor of 1.5 is meaningful; over 30 trades, a strategy with no edge reaches 1.5 by luck 18% of the time.

What is the difference between profit factor and expectancy?

Profit factor is a ratio of total wins to total losses; expectancy is the average result per trade. Two strategies with the same expectancy can have different profit factors, so use expectancy to judge how much a trade earns.

How many trades do I need to trust a profit factor?

Enough for the confidence range of the average trade to sit above zero. For a strategy winning 45% of the time with 1.8R wins and 1R losses, that's about 111 trades at 95% confidence; the backtest sample size calculator works it out for your numbers.

Want our studies and guides first when you search? Add Edgoro as a preferred source on Google.

Build it with Edgoro

Describe your strategy in plain English and get a TradingView indicator in Pine Script v6 or a MetaTrader 5 Expert Advisor in minutes. Every first purchase is covered by a 7-day money-back guarantee.

Build your indicator with Edgoro