Day 72: Profit Factor — Sum of Gainers vs. Losers
The “Just Divide the Sums” Trap
Here’s how a junior engineer ships this feature. They pull the trade log into a list of dicts, loop once to bucket wins and losses, sum each bucket with plain Python floats, and divide:
wins = sum(t["pnl"] for t in trades if t["pnl"] > 0)
losses = sum(t["pnl"] for t in trades if t["pnl"] < 0)
profit_factor = wins / abs(losses)
It passes code review. It passes the demo, because the demo has a curated 20-trade log with at least one loser in it. It ships. Three weeks later the strategy has a hot streak — twelve winning trades in a row, zero losers — and the dashboard throws a ZeroDivisionError in production, at 2:14 AM, during the exact window the strategy is supposed to be scaling into size. The on-call engineer mutes the alert instead of reading the traceback. That’s how accounts get ruined: not by a bad trade, but by a monitoring system that falls over the moment things are going well.
The Failure Mode
There are three distinct failures hiding in that four-line snippet, and they compound:
1. Division by zero is not an edge case, it’s a certainty. Any strategy with a positive edge will, at some point in its life, have a loss-free window. If your metrics layer can’t survive that, it isn’t a metrics layer, it’s a liability with a dashboard attached.
2. Floating-point summation drifts. sum() over a Python list of floats accumulates rounding error linearly with sequence length. Over a session with a few hundred trades, the drift is usually invisible. Over a multi-year backtest replaying tens of thousands of fills, we’ve measured cumulative P&L figures off by several cents on a six-figure book — enough to flip a borderline profit factor from 1.01 to 0.99 and trigger (or fail to trigger) a kill-switch on a coin flip. Money is discrete. It should never live in an IEEE-754 float.
3. The naive version blocks. sum(... for t in trades if ...) re-walks the entire trade history on every single tick if you call it inside a live loop, which is exactly where people call it, because that’s where you want the number. On a volatility spike — the one moment you actually need your risk metrics to be live — your event loop is busy re-summing a growing list instead of processing the next fill. Latency degrades exactly when correctness matters most.



