Most traders can tell you their win rate. Very few can tell you how much they lose, on average, when they lose. That second number decides whether an account grows or bleeds, and you can compute it from a plain export of your trades.
Here is the small script I use to audit a trading log. It needs pandas and a CSV with one row per closed trade.
The data
close_time,symbol,side,pnl,risk
2026-09-01 14:32,XAUUSD,buy,180,100
2026-09-01 16:05,XAUUSD,sell,-100,100
2026-09-02 15:10,US30,buy,-230,100
2026-09-02 15:41,US30,buy,-100,100
2026-09-03 09:20,EURUSD,sell,95,100
2026-09-03 14:50,XAUUSD,buy,-100,100
2026-09-04 15:02,US30,sell,310,100
2026-09-04 15:30,US30,sell,-100,100
2026-09-07 14:45,XAUUSD,buy,-100,
2026-09-07 15:12,XAUUSD,buy,-340,
2026-09-08 10:05,EURUSD,buy,60,100
2026-09-08 15:20,US30,buy,-100,100
pnl is the result of the trade in account currency. risk is what you would have lost if the stop had been hit, also in account currency. Leave it empty when a trade had no stop: that gap is information too.
The script
import pandas as pd
trades = pd.read_csv("trades.csv", parse_dates=["close_time"]).sort_values("close_time")
# 1. Trades with no planned risk (no stop loss) cannot be measured in R
no_stop = trades["risk"].isna()
print(f"Trades without a stop: {no_stop.sum()} of {len(trades)}, "
f"P&L {trades.loc[no_stop, 'pnl'].sum():+.0f}")
# 2. R-multiple: what each trade made or lost, in units of what you planned to risk
measured = trades[~no_stop].copy()
measured["r"] = measured["pnl"] / measured["risk"]
wins = measured[measured["r"] > 0]
losses = measured[measured["r"] <= 0]
print(f"Win rate: {len(wins) / len(measured):.0%}")
print(f"Average win: {wins['r'].mean():+.2f} R")
print(f"Average loss: {losses['r'].mean():+.2f} R")
print(f"Expectancy: {measured['r'].mean():+.2f} R per trade")
# 3. Losses bigger than planned: the stop was moved, widened or ignored
overshoot = losses[losses["r"] < -1]
print(f"Losses beyond -1R: {len(overshoot)}")
# 4. Max drawdown on the equity curve, all trades included
equity = trades["pnl"].cumsum()
drawdown = equity - equity.cummax()
print(f"Max drawdown: {drawdown.min():+.0f}")
# 5. What happens right after a loss
trades["after_loss"] = trades["pnl"].shift(1) < 0
after = trades[trades["after_loss"]]
print(f"Trades taken right after a loss: {len(after)}, P&L {after['pnl'].sum():+.0f}")
What it prints
Trades without a stop: 2 of 12, P&L -440
Win rate: 40%
Average win: +1.61 R
Average loss: -1.22 R
Expectancy: -0.09 R per trade
Losses beyond -1R: 1
Max drawdown: -705
Trades taken right after a loss: 7, P&L -305
How to read it
The win rate is not the problem. 40% sounds bad, but the average win is 1.61R. With losses held at exactly -1R, this log would have an expectancy of about +0.05R per trade: small, but positive.
The losses are the problem. The average loss is -1.22R, not -1R. One trade lost 2.3 times what was planned. That usually means a stop was moved further away "to give the trade room", and that single habit is enough to turn the expectancy negative: -0.09R per trade.
The trades without a stop cost more than anything else. Two trades, -440, more than four planned losses put together. They are invisible in R, which is exactly why they need their own line in the report.
Look at what happens after a loss. Seven of the twelve trades were opened right after a losing one, for a total of -305. If that pattern holds on a bigger sample, the fix is a rule (a pause after a loss), not a new indicator.
Drawdown is measured on everything. The R statistics ignore the trades without a stop, but the equity curve does not. The -705 is what the account actually went through.
Why R and not dollars
R puts every trade on the same scale, whatever the position size or the instrument. A +$300 trade on gold and a +$60 trade on EUR/USD can both be +1R. Once everything is in R, you can compare setups, sessions and weekdays fairly, and you can see when risk discipline slips, because the losses stop being equal to -1.
To go further, group the same calculations by symbol, by hour of the day or by weekday. Three lines of groupby usually say more about a trading week than any chart.
I got tired of doing this by hand every weekend, so I built Tradoshi, a trading journal that computes these numbers on every trade. Its free plan imports your trade history from a file, and the paid plans connect read-only to MetaTrader, cTrader, NinjaTrader or Interactive Brokers. If you would rather keep a CSV and a script, the code above is all you need.
Trading involves a significant risk of capital loss. Nothing in this post is investment advice.
Top comments (2)
The post-loss audit in step five is the most informative metric in the script because it exposes the breakdown of the independent-draw assumption. When expectancy is calculated as a simple arithmetic average across an entire log, it treats trade outcomes as memoryless. If taking a loss shifts the conditional probability of the next trade toward another negative print, trade returns are serially dependent. Drawdowns do not arrive as uniform noise; they cluster, which compresses the distance to the account's ruin barrier much faster than an arithmetic average suggests.
The second hidden tax is the volatility drag between arithmetic R-multiples and geometric compounding. An expectancy of positive zero point zero five R looks viable on a static spreadsheet, but drawdown recovery is asymmetric. A sequence of three losses that takes an account down twenty percent requires a twenty-five percent gain just to reach par. If position sizing does not contract dynamically as equity declines, the portfolio suffers negative compound growth even when the raw trade-level expectancy remains slightly positive.
Overshooting planned risk on stops also carries structural negative skewness that goes beyond trader discipline. While moving or cancelling a stop is behavioral leakage, execution slippage during illiquid prints or gap opens turns a stop into a market order with an unhedged left tail. In volatile regimes, the realized loss distribution develops fat tails that an idealized negative one R model cannot price.
I re-ran your twelve rows and the numbers match: 4 wins of 10 measured trades, +1.61R average win, -1.22R average loss, -0.085R expectancy. One thing I'd add before reading the -0.09R vs +0.05R gap as the stop-moving habit: with only 10 measured trades the R-multiples have a standard deviation of about 1.64, so the standard error of the expectancy is about 0.52R. The 0.14R difference between "as logged" and "losses held at -1R" is a quarter of one standard error. The script is right to surface it, but at this size it can't tell you whether the discipline cost anything. It starts to say something around a few hundred trades.
The -2.3R loss is also worth one more column before blaming a moved stop. It closed at 15:10 on US30, right after the US cash open, where a stop can fill well past its level even if it was never touched. If you log the stop price you planned next to the fill price, you can split "overshoot from slippage" from "overshoot because I widened it", and those need different fixes.
Do you export the planned stop price from your platform, or only the risk figure?