DEV Community

Holdy Lab
Holdy Lab

Posted on

Why 20 Trades Prove Nothing: Sample Size for Trading Behavior (in 25 Lines of Python)

Series: Measuring trader behavior.
Disclosure: I work on Holdy Lab, a behavioral analysis tool for traders. The code below is open (MIT) and you do not need our product to run it. Educational only, not financial advice.*

"My win rate is 60%." Over how many trades? That single question decides whether the number means anything. Traders routinely draw conclusions from 15 or 20 trades, and the same reasoning shows up in "I trade best on Tuesdays" or "I revenge trade after losses". Here is a small way to see how much a number can be trusted before you act on it.

A win rate is a range, not a number

If you win 12 of 20 trades, your observed win rate is 60%. The true win rate of your strategy, the one you would see over thousands of trades, could be quite different. A standard way to express that is a confidence interval. I use the Wilson score interval because it behaves well for small samples and for rates near 0% or 100%, where the textbook formula misbehaves.

from math import sqrt


def wilson(wins: int, n: int, z: float = 1.96) -> tuple[float, float]:
    """95% Wilson score interval for a win rate observed as `wins` out of `n` trades."""
    if n == 0:
        return (0.0, 1.0)
    p = wins / n
    denom = 1 + z * z / n
    centre = (p + z * z / (2 * n)) / denom
    half = z * sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / denom
    return (centre - half, centre + half)


def streak_probability(n_trades: int, streak: int, win_rate: float) -> float:
    """Chance of at least one run of `streak` losses within `n_trades` independent trades."""
    q = 1 - win_rate
    state = [1.0] + [0.0] * (streak - 1)  # state[i] = current losing run has length i and never reached `streak`
    for _ in range(n_trades):
        nxt = [0.0] * streak
        for i, prob in enumerate(state):
            nxt[0] += prob * (1 - q)
            if i + 1 < streak:
                nxt[i + 1] += prob * q
        state = nxt
    return 1 - sum(state)
Enter fullscreen mode Exit fullscreen mode

Running it on a 60% observed win rate:

  20 trades, 60% observed -> plausible true win rate 38.7% to 78.1%
  50 trades, 60% observed -> plausible true win rate 46.2% to 72.4%
 100 trades, 60% observed -> plausible true win rate 50.2% to 69.1%
 200 trades, 60% observed -> plausible true win rate 53.1% to 66.5%
 400 trades, 60% observed -> plausible true win rate 55.1% to 64.7%
Enter fullscreen mode Exit fullscreen mode

After 20 trades, a "60% strategy" could be a coin flip or an excellent system, and the data cannot tell you which. The range narrows slowly: you need roughly four times the trades to halve its width.

Streaks are normal

The same small-sample thinking applies to losing streaks. streak_probability computes the chance of at least one run of N losses somewhere in M trades, exactly, by tracking the current run length as the trades go by.

P(at least 6 losses in a row in 100 trades at 50%) = 54.6%
P(at least 10 losses in a row in 100 trades at 50%) = 4.4%
Enter fullscreen mode Exit fullscreen mode

Even with a 50% win rate, a run of six losses somewhere in 100 trades is more likely than not. If six losses in a row makes you abandon a sound plan, you will keep abandoning plans that were fine.

What to do with this

  1. Report ranges, not points. Write "52% to 70%" in your journal, not "61%".
  2. Set a minimum before judging. Many people use at least 50 trades for an early look and 100 or more before changing a strategy. There is no magic number; the interval tells you how wide your uncertainty still is.
  3. Apply it to behavior, not only to strategy. "I size up after losses" is also an estimate from a sample. Eight trades in each group is a floor, not a comfortable size.
  4. Look for large, repeated effects. Small differences need big samples.

Limits

  • The interval assumes trades are independent and that the strategy does not change. Real markets have regimes, so the true uncertainty is larger than the table shows.
  • The streak calculation assumes a constant win rate and independent trades.
  • Neither tells you why a result happened.

The tests and two more checks (a prop-firm Monte Carlo and a weekday permutation test) are in the open repository. If you would rather not run code, the free Trade Analyzer labels results by sample size, and how many trades you need to evaluate a strategy explains the same idea without code.

Top comments (1)

Collapse
 
stratcorealpha profile image
Arnold Holm •

Your independence limit has a large effect even at the same 50% marginal loss rate. I ran a two-state toy model for 100 trades: six consecutive losses occur with probability 54.6% when trades are independent, and 99.1% when P(loss after loss)=0.8 and P(loss after win)=0.2. Both models start in the stationary 50/50 state. Those are assumed probabilities, not market estimates. I would expose the two transition probabilities alongside win_rate so readers can test the clustering assumption explicitly.