DEV Community

Holdy Lab
Holdy Lab

Posted on

How to measure revenge trading from a trade history (in 40 lines of Python)

Disclosure: I work on Holdy Lab, a behavioral analysis tool for traders. The method below is the same one our Trade Analyzer uses, simplified. You do not need our product to run it.

Everyone agrees revenge trading is bad. Almost nobody measures it. Journals say "I broke my rule", which is a statement about one trade. The useful question is whether it happens systematically: after a loss, do you trade differently from after a win?

That is measurable from a plain trade history. Here is a small method, its reasoning and its limits.

The two signatures

Revenge trading is usually described as trading to win back a loss, with a larger size, faster entry and weaker setups. Two of those can be read straight from exported trades:

  1. Position size after a loss versus after a win.
  2. Waiting time before the next trade after a loss versus after a win.

The key design choice: compare a trader with themselves, split by what the previous trade did. A trader who always sizes big, or always trades fast, is not revenge trading. A trader whose size and speed change after a loss, relative to after a win, might be.

Why medians, why "after a win" as the control

  • Medians, not means. One oversized trade should not decide the result.
  • Control group = trades after a win. Comparing with all other trades mixes in the first trade of a session, trades after breakeven and so on. Win versus loss isolates the one thing you want to test.
  • Minimum group size. With fewer than about eight trades on either side, a median is noise. The code refuses to answer instead of guessing.

The code

import pandas as pd

MIN_GROUP = 8          # trades needed on each side of the comparison
SIZE_CHANGE = 0.15     # size after a loss at least 15% above size after a win
REENTRY_SPEEDUP = 0.25 # wait after a loss at least 25% shorter than after a win


def revenge_signs(trades: pd.DataFrame) -> dict:
    """trades needs: opened_at, closed_at (datetimes), pnl, notional (position value)."""
    t = trades.sort_values("opened_at").reset_index(drop=True)
    prev_pnl = t["pnl"].shift(1)
    after_loss = t[prev_pnl < 0]
    after_win = t[prev_pnl > 0]
    out = {"n_after_loss": len(after_loss), "n_after_win": len(after_win)}
    if min(out["n_after_loss"], out["n_after_win"]) < MIN_GROUP:
        out["verdict"] = "not enough trades to say anything"
        return out

    size_change = after_loss["notional"].median() / after_win["notional"].median() - 1
    gap = (t["opened_at"] - t["closed_at"].shift(1)).dt.total_seconds()
    wait_loss = gap[(prev_pnl < 0) & (gap >= 0)].median()
    wait_win = gap[(prev_pnl > 0) & (gap >= 0)].median()
    speedup = 1 - wait_loss / wait_win

    out.update(size_change=round(float(size_change), 3), reentry_speedup=round(float(speedup), 3))
    out["size_sign"] = bool(size_change >= SIZE_CHANGE)
    out["reentry_sign"] = bool(speedup >= REENTRY_SPEEDUP)
    return out
Enter fullscreen mode Exit fullscreen mode

Input is a table with when each trade opened and closed, its profit or loss, and its position value. Most exchange exports contain these or something close to them.

The thresholds (15% bigger size, 25% shorter wait) are choices, not laws. They are deliberately conservative so that a small drift does not get labelled. Change them and see how the verdict moves.

What it cannot tell you

  • Intent. It sees behavior, not motive. Fast re-entry after a loss can be a legitimate plan.
  • Stop-loss behavior and entry context. Exports rarely include stop orders or what the price did before your entry, so it cannot see moved stops or chasing a move.
  • Small samples. Under about 50 trades, read any result as an early sign. Under 20, treat it as nothing.
  • Causality. It says your size and speed differ after losses. It does not say that is why you lost money.

What to do with the result

Do not treat a positive result as a verdict. Treat it as a hypothesis to test: write one rule (a pause after a loss, a size cap), apply it for a fixed number of trades, then run the same measurement again. If the numbers move and hold, the rule works for you. If not, change the rule, not your opinion of yourself.

If you would rather not run code, our free Trade Analyzer runs these two checks and thirteen more on an exported file, and processes it in memory without storing the file. The longer explanation of how we build findings is in our write-up of the framework.

Not financial advice. This measures past behavior in the data you give it and does not predict results.

Top comments (0)