A 92% win rate and a blown account are not a contradiction. They are the standard outcome of one specific strategy, and the account's owner usually found out about the second number a few weeks after being very proud of the first.
Win rate, profit factor, and expectancy each answer a different question, and each one is misleading on its own. This is the sequel to the Sharpe ratio and max drawdown post: same approach, the three per-trade metrics that every strategy report shows and almost nobody reads correctly.
The three formulas
Win rate is the share of closed trades with a positive net result:
win_rate = winning_trades / closed_trades
Profit factor is how many dollars you made for each dollar you lost:
profit_factor = gross_profit / gross_loss
where gross profit is the sum of all winning trades' net and gross loss is the absolute sum of all losing trades' net. Above 1.0 the strategy made money. Exactly 1.0 is breakeven. The profit factor calculation has no concept of how many trades produced those sums, which is the trap covered below.
Expectancy is the average net result per trade, and the formula everyone quotes is just the average written in terms of the other two:
expectancy = (win_rate × average_win) - (loss_rate × average_loss)
= net_profit / closed_trades
Both lines give the same number (the first is the second with the sum split into winners and losers). The second is easier to compute and harder to get wrong, because it doesn't tempt you to plug in "average win" from a different window than "win rate".
Expectancy in currency depends on position size, which makes it useless for comparing a $5,000 account to a $500,000 one. Divide by the amount risked per trade and you get expectancy in R: the average result as a multiple of the planned risk. An expectancy of 0.2R means every trade, on average, earns a fifth of what you were willing to lose on it.
Ten trades, all three numbers
Each trade risked $100 (that's 1R). Net results after commission and swap:
| # | Net | R |
|---|---|---|
| 1 | +120 | +1.20 |
| 2 | -80 | -0.80 |
| 3 | +200 | +2.00 |
| 4 | -60 | -0.60 |
| 5 | +90 | +0.90 |
| 6 | -150 | -1.50 |
| 7 | +45 | +0.45 |
| 8 | -70 | -0.70 |
| 9 | +160 | +1.60 |
| 10 | -40 | -0.40 |
Trade 6 lost more than 1R because the stop slipped through a news spike, which is normal and exactly why "I only ever lose 1R" belongs in the plan, not the report.
winning trades = 5 win_rate = 50%
gross profit = 615 average_win = 123.00
gross loss = 400 average_loss = 80.00
profit_factor = 615 / 400 = 1.54
net_profit = 215
expectancy = 0.5 × 123 - 0.5 × 80 = 21.50 per trade
expectancy (R) = 21.50 / 100 = 0.215R
Ten trades. Half won. Each trade earned $21.50 on average, or a fifth of its risk. Every one of those numbers is correct, and none of them tells you whether this strategy works, for a reason that comes later.
Win rate on its own is meaningless
The 92% account from the opening. The strategy: buy, take $50 of profit, and if price goes against you, don't close, add. Wait for it to come back. It usually does.
Nine trades make $50 each: $450. The tenth doesn't come back and the doubled-up position gets stopped out at -$1,000. Net over ten trades: -$550. Win rate 90%. Profit factor 0.45. Expectancy -$55 per trade.
The win rate wasn't lying, exactly. It just doesn't have a column for size. Any strategy that takes small, frequent wins and rare, large losses has a high win rate and can have any profit factor at all, including the ones below 1.0. Martingale, grid, and most "sell options for income" approaches live here.
The reverse is also true and less discussed: a 35% win rate with average wins three times the average losses is expectancy of 0.35 × 3 - 0.65 × 1 = +0.40R, a solidly profitable trend-following profile that feels like losing most days. Traders quit those strategies because the win rate hurts, not because the math does.
Profit factor without a trade count is meaningless
A profit factor of 3.0 sounds excellent. Here's one: six wins of $150 and two losses of $150 over eight trades. Gross profit 900, gross loss 300, PF 3.0.
Add one more loss, a perfectly ordinary $300 (a 2R loss, the kind trade 6 above produces). Gross loss becomes 600 and PF drops to 1.5. One trade halved it, because with eight trades, one trade is an eighth of the sample.
A strategy reporting PF 3.0 over eight trades and one reporting PF 1.5 over eight trades are statistically indistinguishable. Neither number has settled yet. Profit factor is a ratio of two sums, and sums built from a handful of trades swing with every addition.
The sample-size problem, with numbers
Win rate is a proportion, and proportions have a standard error you can compute on a napkin:
standard_error = sqrt(p × (1 - p) / n)
95% interval ≈ p ± 1.96 × standard_error
For the 50% win rate above:
| Trades | 95% interval on a 50% win rate |
|---|---|
| 10 | 19% to 81% |
| 50 | 36% to 64% |
| 100 | 40% to 60% |
| 400 | 45% to 55% |
Ten trades tells you the win rate is somewhere between "terrible" and "remarkable". A hundred narrows it to something you'd act on. Four hundred pins it.
Profit factor has no clean formula because it's a ratio of sums of skewed values, so bootstrap it: resample your trades with replacement a few thousand times, compute PF each time, take the 5th and 95th percentiles. Do that on the ten trades above and the interval runs from well under 1.0 to above 3.0, which is the honest description of "PF 1.54 over ten trades". At a few hundred trades the same exercise gives an interval you can actually use.
Breakeven trades and partial closes
Two counting decisions change the numbers more than people expect.
A trade with net exactly zero is neither a win nor a loss. It belongs in
closed_trades (it happened, it used time and margin) and in the win-rate
denominator, but in neither gross figure. Excluding it from the denominator
inflates win rate; counting it as a win inflates it more. Trades that net to
exactly zero after commission are rare, so this rarely matters, but the rule
should be explicit in your code.
Partial closes matter more. A position scaled out in three chunks with +50, +30, and -20 is one trade with a net of +60, not two wins and a loss. Count the chunks and you've tripled the trade count, pushed the win rate up, and made the sample look three times larger than it is. Group deals by position id first, sum everything in the group, then classify. The orders, deals and positions post covers the grouping; the trade costs post covers why "everything" includes commission and swap.
What good looks like, honestly
Over hundreds of trades, after costs, a profit factor between 1.3 and 2.0 is a real edge and a hard thing to have. Most systematic strategies that survive several years sit in that band. Expectancy of 0.1R to 0.4R is the same story in different units.
A profit factor of 5.0 is almost always one of three things: a small sample, a strategy that hasn't met its losing regime yet, or a backtest with look-ahead. Occasionally it's a genuine short-lived inefficiency, and those compress toward the band above as soon as size or competition arrives.
The test is whether the number holds as the trade count grows. PF 1.6 at 100 trades that is still 1.5 at 500 is worth more than PF 4.0 at 30 trades, and it is not close.
How /performance returns these
/v1/accounts/{id}/performance computes all three from reconstructed trades,
not raw deals: deals grouped by position id, net summed across profit,
commission, swap, and fee for every deal in the group, open positions
skipped. A win is net > 0, a loss is net < 0, and a net-zero trade counts
in closed_trades and the win-rate denominator only.
The payload carries win_rate (a percentage), profit_factor, expectancy
(in account currency per trade, since the API doesn't know your planned risk),
closed_trades, average_win, average_loss, and win_loss_ratio.
profit_factor is null rather than infinity when there are no losing trades
in the range, and win_loss_ratio is null when there are no losses to
average. Treat a null there as "not enough data", which is what it is.
import math
import os
import requests
BASE = "https://api.metakit.cloud"
HEADERS = {"Authorization": f"Bearer {os.environ['METAKIT_KEY']}"}
perf = requests.get(
f"{BASE}/v1/accounts/2/performance",
params={"range": "12m"},
headers=HEADERS,
).json()
n = perf["closed_trades"]
p = perf["win_rate"] / 100
half_width = 1.96 * math.sqrt(p * (1 - p) / n) if n else float("nan")
print(f"closed trades {n}")
print(f"win rate {perf['win_rate']:.1f}% (±{half_width * 100:.1f} pts at 95%)")
print(f"profit factor {perf['profit_factor']}") # None if no losing trades yet
print(f"expectancy {perf['expectancy']:.2f} per trade")
print(f"avg win/loss {perf['average_win']:.2f} / {perf['average_loss']:.2f}")Print the interval next to the win rate every time. It's the cheapest possible
defence against a dashboard that makes ten trades look like a track record. The
full payload, including the sessions series that Sharpe is built from, is
documented in the API reference.
Next time a strategy report shows you a profit factor, find the trade count before you find anything else. If it isn't there, the profit factor isn't either.