The Quest Begins (The "Why")
Honestly, I still remember the first time I watched my algo blow up a simulated account because I’d sized every trade like I was betting on a coin flip. I’d coded a slick mean‑reversion strategy, back‑tested it to perfection, and then—poof—a single adverse tick wiped out weeks of paper profits. I felt like Neo staring at a barrage of bullets, except I had no dodge move.
The problem wasn’t the signal; it was the risk. I was allocating a fixed % of capital per trade, ignoring volatility, and slapping a static stop loss that made no sense when markets went from calm to chaos in minutes. I kept asking myself: “How do the pros stay alive when the market decides to go full‑on boss mode?” That question sent me down a rabbit hole of position sizing models and dynamic stop techniques, and honestly, it felt like unlocking a new skill tree.
The Revelation (The Insight)
The breakthrough came when I realized two simple truths:
- Position size should adapt to the risk of each trade, not to a fixed fraction of equity.
- Stop losses ought to breathe with the instrument’s volatility, otherwise you’re either getting stopped out too early or giving the market too much rope.
If you treat every trade as if it had the same risk profile, you’re essentially playing roulette with a weighted wheel. The Kelly criterion, volatility‑adjusted fixed fractional, and ATR‑based stops are the tools that turn that roulette into a calculated game.
I started by measuring the average true range (ATR) of the asset over the last N periods. ATR gives you a sense of how much the price typically moves. Then I sized each position so that a loss equal to, say, 1 × ATR would represent a fixed % of my equity (often 1 % or 2 %). The stop loss itself was placed at a multiple of ATR away from the entry—usually 1.5 × ATR for longs and the same for shorts.
Suddenly, my equity curve stopped looking like a rollercoaster designed by a sadist and started resembling a steady climb with occasional, manageable dips. It was like finally getting the right combo in a fighting game: you block, you counter, you win.
Wielding the Power (Code & Examples)
Below is a Python snippet that shows the before (naïve fixed fraction) and the after (volatility‑adjusted sizing + ATR stop). I’ve deliberately left in a couple of common pitfalls so you can spot the traps on your own quest.
import pandas as pd
import numpy as np
# -------------------------------------------------
# Sample data: OHLCV for a fictitious future contract
# -------------------------------------------------
df = pd.DataFrame({
'open': np.random.randn(100).cumsum() + 100,
'high': np.random.randn(100).cumsum() + 102,
'low': np.random.randn(100).cumsum() + 98,
'close': np.random.randn(100).cumsum() + 100,
'volume': np.random.randint(1000, 5000, size=100)
})
df['high'] = df[['open','close']].max(axis=1) + np.random.rand(100)*0.5
df['low'] = df[['open','close']].min(axis=1) - np.random.rand(100)*0.5
# -------------------------------------------------
# 1️⃣ THE STRUGGLE: Fixed‑fraction position sizing
# -------------------------------------------------
equity = 100_000 # starting capital
risk_per_trade = 0.01 # 1% of equity per trade (naïve)
fixed_frac = risk_per_trade # we use the same fraction every time
# Simple entry signal: buy when close > 20‑period SMA, sell when < SMA
df['sma20'] = df['close'].rolling(20).mean()
df['signal'] = np.where(df['close'] > df['sma20'], 1, -1) # 1 = long, -1 = short
# Fixed‑fraction shares (ignore volatility!)
df['shares_fixed'] = (equity * fixed_frac) / df['close']
df['shares_fixed'] = df['shares_fixed'].astype(int) # whole contracts
# Static stop loss: 50 ticks away (no adaptation to volatility)
tick_size = 0.25 # example tick size
static_stop_ticks = 50
df['stop_price_fixed'] = np.where(df['signal'] == 1,
df['close'] - static_stop_ticks * tick_size,
df['close'] + static_stop_ticks * tick_size)
# -------------------------------------------------
# 2️⃣ THE TRAPS (common mistakes) – spot them!
# -------------------------------------------------
# Trap A: Using the *current* equity for sizing after each trade
# (instead of the equity *at the time of the signal*).
# This can cause over‑sizing during a winning streak.
#
# Trap B: Placing the stop based on a fixed tick distance
# regardless of how volatile the instrument is.
# In low‑vol periods you get stopped out too often;
# in high‑vol periods you give the market too much room.
# -------------------------------------------------
# 3️⃣ THE VICTORY: Volatility‑adjusted sizing + ATR stop
# -------------------------------------------------
# Calculate ATR (14‑period)
high_low = df['high'] - df['low']
high_close = np.abs(df['high'] - df['close'].shift())
low_close = np.abs(df['low'] - df['close'].shift())
tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
df['atr14'] = tr.rolling(14).mean()
# Risk per trade in dollars (still 1% of equity)
risk_dollars = equity * risk_per_trade
# Volatility‑adjusted position size: risk_dollars / (ATR * multiplier)
# We'll use 1 × ATR as the stop distance for sizing.
atr_multiplier_for_size = 1.0
df['shares_vol'] = (risk_dollars / (df['atr14'] * atr_multiplier_for_size)).astype(int)
# Dynamic stop loss: place stop at ATR_multiplier_stop × ATR from entry
atr_multiplier_stop = 1.5
df['stop_price_vol'] = np.where(df['signal'] == 1,
df['close'] - atr_multiplier_stop * df['atr14'],
df['close'] + atr_multiplier_stop * df['atr14'])
# -------------------------------------------------
# 4️⃣ Quick equity curve comparison (optional)
# -------------------------------------------------
def compute_equity(df, shares_col, stop_col):
equity_curve = [equity]
position = 0
entry_price = 0.0
for i in range(len(df)):
if position == 0 and df['signal'].iloc[i] != 0:
# open new position
position = df['signal'].iloc[i] * df[shares_col].iloc[i]
entry_price = df['close'].iloc[i]
elif position != 0:
# check stop
stop_price = df[stop_col].iloc[i]
if (position > 0 and df['low'].iloc[i] <= stop_price) or \
(position < 0 and df['high'].iloc[i] >= stop_price):
# stop hit
pnl = position * (stop_price - entry_price)
equity_curve.append(equity_curve[-1] + pnl)
position = 0
entry_price = 0.0
continue
# optional: close on opposite signal
if df['signal'].iloc[i] != position:
pnl = position * (df['close'].iloc[i] - entry_price)
equity_curve.append(equity_curve[-1] + pnl)
position = 0
entry_price = 0.0
continue
# hold
equity_curve.append(equity_curve[-1])
return pd.Series(equity_curve, name='equity')
eq_fixed = compute_equity(df, 'shares_fixed', 'stop_price_fixed')
eq_vol = compute_equity(df, 'shares_vol', 'stop_price_vol')
# Plot (if you have matplotlib) – omitted for brevity
# eq_fixed.plot(label='Fixed‑frac')
# eq_vol.plot(label='Vol‑adjusted')
# plt.legend()
# plt.show()
What changed?
-
Position size now reacts to how jumpy the market is (
ATR). When volatility spikes, the denominator grows, shrinking the trade size automatically—no more accidentally over‑leveraging during a news‑driven surge. - Stop loss is no longer a rigid tick count; it widens and narrows with ATR, giving the trade room to breathe when the market is noisy and tightening up when things calm down.
The equity curve from the volatility‑adjusted version shows far less drawdown, and the win‑rate stays roughly the same because we’re not throwing away good signals—we’re just managing their risk better.
Why This New Power Matters
Now you’ve got a risk‑aware trading engine that treats each signal like a unique boss fight instead of a generic grunt. You can scale up confidence in high‑edge, low‑vol environments and automatically dial back when the market throws a curveball.
Imagine deploying this across a basket of futures, crypto pairs, or even equities: the same code adapts to each instrument’s personality without manual tweaking. That’s the kind of robustness that lets you sleep through the night while your algos keep grinding.
And the best part? You don’t need a PhD in statistics—just a few lines of pandas, a clear risk percent, and the willingness to let volatility do the heavy lifting.
Your Turn – The Challenge
Grab a symbol you like, slap on a simple moving‑average crossover (or any signal you trust), and replace the naïve fixed‑fraction sizing with the ATR‑based version above. Plot both equity curves side‑by‑side and watch the difference.
What did you notice? Did the volatility‑adjusted curve hug the steady climb you were hoping for, or did you spot a new tweak that made it even better? Drop your observations in the comments—let’s learn from each other’s quests!
Happy hunting, and may your stops always be just far enough away to let the winners run. 🚀
Top comments (0)