DEV Community

Timevolt
Timevolt

Posted on

Risk Management in Algo Trading: Lessons from The Matrix

The Quest Begins (The "Why")

I still remember the first time I watched my algo blow up a simulated account in under an hour. I had spent weeks polishing a mean‑reversion signal, tuned the entry logic until it sang, and then — boom — the position size I’d hard‑coded at 10 % of equity turned a modest drawdown into a margin call. The chart looked like a roller‑coaster that had lost its brakes, and I felt like I’d just missed the final boss in a game because I forgot to bring a health pack.

That moment forced me to ask: If my edge is real, why am I still losing? The answer wasn’t in a better indicator; it was in how much I risked on each trade and how I protected myself when the market decided to surprise me. In other words, I needed a proper risk‑management layer — position sizing and stops — before I could trust any signal at all.

The Revelation (The Insight)

The breakthrough came when I stopped thinking about “how many shares to buy” and started thinking about “how much of my equity am I willing to lose on this idea.” Two simple ideas changed everything:

  1. Position sizing as a fraction of equity – Instead of a fixed number of contracts, I size each trade so that a stop‑loss hit would remove only a small, predefined slice of my capital (often 1 %–2 %). This makes the strategy survive streaks of losers without blowing up.
  2. Stops that adapt to volatility – A static stop (e.g., always 50 pips) works in calm markets but gets whipsawed when volatility spikes. Using an ATR‑based stop (Average True Range) lets the stop breathe with the market, giving the trade room to move while still cutting losses quickly when the price moves against me.

Together, these ideas turned my trading from a gamble into a repeatable process. It felt like discovering a cheat code from Contra — suddenly everything made sense, and I could focus on refining the edge instead of watching the account bleed.

Wielding the Power (Code & Examples)

The Naïve Approach (the struggle)

# naive_fixed_size.py
import pandas as pd

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

def execute_trades(df, equity=1, equity_start=100_000):
    position = 0          # number of contracts/shares
    cash = equity_start
    equity_curve = []

    for i, row in df.iterrows():
        # enter on signal
        if row['signal'] == 1 and position == 0:
            # fixed 10% of equity, ignore volatility
            notional = equity_start * 0.10
            price = row['close']
            position = notional / price
            cash -= notional          # simplify: ignore commissions

        # exit on opposite signal
        elif row['signal'] == -1 and position > 0:
            cash += position * row['close']
            position = 0

        equity_curve.append(cash + position * row['close'])

    df['equity'] = equity_curve
    return df
Enter fullscreen mode Exit fullscreen mode

The problems?

  • Fixed fraction of initial equity ignores that equity changes; after a few losses you’re still risking the same absolute amount.
  • No stop‑loss means a single adverse move can erase weeks of profit.
  • Static size doesn’t respect volatility — during a high‑vol regime you might be over‑leveraged, during low‑vol you’re under‑utilizing capital.

The Upgraded Approach (the victory)

# risk_managed.py
import pandas as pd
import numpy as np

def atr(df, period=14):
    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)
    return tr.rolling(period).mean()

def generate_signals(df):
    df['signal'] = np.where(df['close'].rolling(20).mean() > df['close'].rolling(50).mean(), 1, 0)
    df['signal'] = df['signal'].diff().fillna(0)   # 1 on entry, -1 on exit
    return df

def position_size(equity, risk_per_trade, stop_distance):
    """
    equity      : current account equity
    risk_per_trade: fraction of equity we are willing to lose (e.g., 0.01 for 1%)
    stop_distance: price difference between entry and stop (in same units as price)
    returns number of shares/contracts to trade
    """
    dollar_risk = equity * risk_per_trade
    if stop_distance == 0:
        return 0
    return dollar_risk / stop_distance

def execute_trades(df, equity_start=100_000, risk_per_trade=0.01, atr_mult=1.5):
    df = generate_signals(df)
    df['atr'] = atr(df)

    equity = equity_start
    position = 0
    entry_price = 0
    stop_price = 0
    equity_curve = []

    for i, row in df.iterrows():
        price = row['close']

        # --- ENTRY LOGIC ---
        if row['signal'] == 1 and position == 0:
            # volatility‑based stop: ATR * multiplier below entry for longs
            stop_distance = row['atr'] * atr_mult
            size = position_size(equity, risk_per_trade, stop_distance)
            if size > 0:
                position = size
                entry_price = price
                stop_price = entry_price - stop_distance   # long stop below entry

        # --- EXIT LOGIC ---
        elif position > 0:
            # hit stop?
            if price <= stop_price:
                equity += position * stop_price   # realize loss at stop
                position = 0
            # or signal reversal?
            elif row['signal'] == -1:
                equity += position * price
                position = 0

        equity_curve.append(equity + position * price)

    df['equity'] = equity_curve
    return df
Enter fullscreen mode Exit fullscreen mode

What changed?

  • Dynamic position sizing: each trade risks only risk_per_trade (1 % here) of the current equity. The size shrinks after losses and grows after wins, keeping the risk profile stable.
  • ATR‑based stop: the stop distance scales with recent volatility (atr_mult * ATR). In choppy markets the stop widens, preventing premature exits; in quiet markets it tightens, protecting capital.
  • Clear entry/exit: we only enter when we have a signal and no open position; we exit either on stop or on an opposite signal.

Common traps to avoid (the “boss level” moments):

  1. Using the initial equity for sizing – always recompute size with the latest equity, otherwise you’ll over‑risk after a drawdown.
  2. Setting atr_mult too low – a stop that’s too tight will get hit by normal noise, turning your strategy into a high‑frequency loss machine. Test a range (1.0‑2.0) on your instrument.
  3. Forgetting to update the stop after entry – if you move the stop to break‑even or trail it, make sure the new stop distance still reflects the current ATR or a trailing‑ATR logic.

Run both scripts on the same historical data and you’ll see the equity curve of the naïve version swing wildly, sometimes dipping below zero, while the risk‑managed version stays smooth, with draw‑downs limited to roughly the chosen risk_per_trade per trade.

Why This New Power Matters

With these two tools in my belt, I stopped chasing “holy grail” indicators and started focusing on what truly drives survival: controlled risk. The strategy now feels less like a gamble and more like a disciplined business — each trade is a calculated investment with a known worst‑case loss.

That confidence lets me:

  • Allocate capital across multiple strategies without fear that one blow‑up will take down the whole portfolio.
  • Sleep at night knowing that a single bad day won’t erase weeks of work.
  • Iterate faster on the signal side because the risk layer is stable; I can experiment with entries, exits, or filters and instantly see the impact on profitability rather than being masked by massive, random swings.

In short, position sizing and volatility‑adjusted stops transformed my algo from a fragile prototype into a robust trading system.

Your Turn – The Challenge

Grab a dataset (forex, futures, or even crypto) and implement the risk‑managed version above. Try varying risk_per_trade (0.5 %, 1 %, 2 %) and atr_mult (1.0, 2.0) and observe how the equity curve changes.

Question for you: What’s the biggest draw‑down you’ve ever seen in a strategy, and how do you think a proper position‑sizing rule would have changed that outcome?

Share your results or thoughts in the comments — let’s learn from each other’s quests! 🚀

Top comments (0)