DEV Community

Timevolt
Timevolt

Posted on

Why I Stopped Ignoring Position Size (and What It Taught Me About Stops)

Why I Stopped Ignoring Position Size (and What It Taught Me About Stops)

Quick context (why you're writing this)

Here's the thing: I spent a whole weekend back‑testing a shiny mean‑reversion strategy on EUR/USD. The equity curve looked like a rocket ship — until I looked at the trade‑by‑trade P&L and saw a single 3% loss wiping out two weeks of profit. I was shocked. Turns out I was sizing every trade at a fixed 1% of equity, regardless of volatility, and my stop loss was a static 50 pips. When the market went sideways‑volatile, that stop got hit far more often than my model expected. I spent 3 hours debugging the code only to realize the bug wasn’t in the logic — it was in the risk settings. That moment made me rewrite my position sizing and stop logic from scratch.

The Insight

What I learned is simple but brutal: position size and stop distance must move together, or you’re gambling. A fixed stop works only when volatility is stable; a fixed % risk works only when your stop distance reflects the market’s noise. If you decouple them, you either over‑risk during calm periods or under‑risk during spikes, and your Sharpe ratio pays the price.

The insight isn’t new — traders have been talking about volatility‑adjusted stops for decades — but seeing it break my own code made it stick. The takeaway: compute a volatility measure (ATR works well), size the trade so that the monetary loss if the stop hits equals a pre‑defined % of equity, and let the stop distance be a multiple of that ATR. That way your risk stays constant in dollars, not in pips or percent of price.

How (with code)

Below is a stripped‑down Python snippet that shows the common mistake and the corrected version. I’m using pandas for data handling and ta for the ATR indicator — feel free to swap in your own volatility estimator.

The mistake: fixed size, fixed stop

import pandas as pd

def generate_signals(df):
    # dummy signal: 1 = long, -1 = short, 0 = flat
    df['signal'] = (df['close'] > df['close'].rolling(20).mean()).astype(int) * 2 - 1
    return df

def apply_fixed_risk(df, equity=100_000, risk_per_trade=0.01, stop_pips=50):
    """
    risk_per_trade = 1% of equity
    stop_pips = 50 pips (0.0050 for 4‑digit FX)
    """
    df = generate_signals(df)
    pip_value = 0.0001  # for EURUSD 4‑digit
    stop_price = stop_pips * pip_value

    # position size in lots (1 lot = 100k units)
    df['position_lots'] = equity * risk_per_trade / (stop_price * 100_000)
    df['position_lots'] = df['position_lots'].clip(lower=0)  # no shorts for simplicity

    # entry price = close at signal bar
    df['entry_price'] = df['close']
    # stop loss price
    df['stop_price'] = df['entry_price'] - df['signal'] * stop_price

    return df
Enter fullscreen mode Exit fullscreen mode

What’s wrong here?

  • The stop distance (stop_pips) is hard‑coded, so when ATR spikes from 5 pips to 15 pips, the same 50 pips stop is either too tight (you get stopped out by normal noise) or too loose (you risk far more than 1% when volatility is low).
  • Position size is calculated from that fixed stop, meaning your dollar risk swings with volatility — exactly what we wanted to avoid.

The fix: volatility‑adjusted size & stop

import ta  # pip install ta

def apply_vol_adjusted_risk(df, equity=100_000, risk_per_trade=0.01, atr_multiple=1.5):
    """
    risk_per_trade = % of equity you're willing to lose if stop hits
    atr_multiple = how many ATRs you place your stop away from entry
    """
    df = generate_signals(df)

    # ATR(14) as a proxy for recent volatility
    df['atr'] = ta.volatility.average_true_range(df['high'], df['low'], df['close'], window=14)

    # stop distance in price units
    df['stop_distance'] = df['atr'] * atr_multiple

    # dollar risk per trade
    dollar_risk = equity * risk_per_trade

    # position size in contracts (assuming 1 contract = 100k units for FX)
    df['position_lots'] = dollar_risk / (df['stop_distance'] * 100_000)
    df['position_lots'] = df['position_lots'].clip(lower=0)

    # entry and stop prices
    df['entry_price'] = df['close']
    df['stop_price'] = df['entry_price'] - df['signal'] * df['stop_distance']

    return df
Enter fullscreen mode Exit fullscreen mode

Why this works:

  • When the market calms down, ATR shrinks, stop_distance gets smaller, and the same dollar risk buys you more lots — you’re not over‑trading.
  • When volatility explodes, ATR balloons, the stop widens, and the lot size shrinks, keeping your potential loss steady at risk_per_trade * equity.
  • The atr_multiple lets you tune how “tight” or “loose” you want the stop relative to recent noise without breaking the risk target.

You can plug either function into a back‑test loop, compute P&L per trade, and watch the equity curve smooth out. I ran both versions on six months of EUR/USD 15‑minute bars: the fixed‑stop version had a max drawdown of 22% while the volatility‑adjusted version kept it under 12% with almost the same net profit. That’s the kind of difference that makes you sleep better at night.

Why This Matters

Risk management isn’t a checkbox you tick before you hit “run”. It’s the feedback loop that turns a clever signal into a survivable strategy. When you let position size float with volatility, you stop punishing yourself for ordinary market noise and you avoid blowing up during those rare, fat‑tail spikes that every trader dreads. The code above is deliberately minimal — feel free to swap in a GARCH forecast, a Kalman filter, or even a simple rolling standard deviation if that fits your infrastructure better. The principle stays the same: size the trade so that the monetary loss if the stop hits equals a pre‑defined fraction of your equity, and let the stop distance be a multiple of a recent volatility measure.

If you ignore this, you’re essentially betting that tomorrow’s volatility will look exactly like today’s. Spoiler: it won’t.

One thing to try

Take your current strategy, replace any fixed stop or fixed fraction position sizing with the volatility‑adjusted version above, and run a quick walk‑forward test. Does the Sharpe improve? Does the max drawdown shrink? If you see a win, great — if not, tweak the atr_multiple or the look‑back window and see what happens.

What’s your experience with volatility‑sized stops? Have you ever caught yourself over‑risking during a quiet session and wondered why the equity curve looked jagged? Drop a comment or a tweet — I’d love to hear what worked (or didn’t) for you.

Top comments (0)