DEV Community

Timevolt
Timevolt

Posted on

Trading Like Neo: Building a Profitable Algorithm

The Quest Begins (The "Why")

Honestly, I started this whole thing because I was tired of watching my savings sit idle while the market did its thing. I’d read a few blog posts, copy‑pasted some Python snippets, and watched my “strategy” lose money faster than a leaky bucket. The moment that really stuck with me was when I realized I was chasing signals without any idea of what they actually meant—like trying to defeat a boss without learning its attack pattern. I needed a dragon to slay, and that dragon was over‑trading fueled by naïve signals.

The Revelation (The Insight)

The breakthrough came when I stopped treating the algorithm as a magic black box and started thinking about it as a disciplined trader. The key insight? Profitability isn’t about catching every tiny move; it’s about letting the winners run and cutting the losers short—while keeping the execution simple enough to trust.

In plain English:

  1. Signal quality > quantity – a moving‑average crossover works, but only when you filter out choppy markets.
  2. Risk management is non‑negotiable – position size based on volatility, a hard stop, and a max‑daily‑loss rule.
  3. Walk‑forward validation beats in‑sample over‑fitting – test on data you haven’t seen, then re‑train on a rolling window.

When I internalized those three points, the equity curve went from a jagged mess to a smooth upward slope. It felt like finding the One Ring—except instead of invisibility, I got consistency.

Wielding the Power (Code & Examples)

Below is a before/after look at a simple moving‑average crossover strategy. The “before” version is what I started with: pure signals, no risk checks. The “after” version adds the discipline that turned it into a profit‑maker.

The Struggle – Naïve Crossover

import pandas as pd
import yfinance as yf

# download data
df = yf.download('AAPL', start='2018-01-01', end='2023-01-01')

# signals: short MA > long MA => buy, else sell
df['short_ma'] = df['Close'].rolling(20).mean()
df['long_ma']  = df['Close'].rolling(50).mean()
df['signal']   = 0
df.loc[df['short_ma'] > df['long_ma'], 'signal'] = 1   # long
df.loc[df['short_ma'] < df['long_ma'], 'signal'] = -1  # short

# naive execution: always 100% equity, no stop
df['position'] = df['signal'].shift(1)   # avoid look‑ahead bias
df['returns']  = df['Close'].pct_change()
df['strategy'] = df['position'] * df['returns']
df['cum']      = (1 + df['strategy']).cumprod()
Enter fullscreen mode Exit fullscreen mode

What went wrong?

  • No filter – the strategy flips in sideways markets, generating a whirlwind of trades.
  • Fixed size – each trade risks the same amount regardless of volatility, blowing up during high‑vol regimes.
  • No stop‑loss – a single adverse move can erase weeks of profit.

The Victory – Discipline Added

import numpy as np

# ---------- PARAMETERS ----------
FAST = 20
SLOW = 50
ATR_PERIOD = 14          # for volatility‑based sizing
RISK_PER_TRADE = 0.01    # 1% of equity per trade
MAX_DAILY_LOSS = 0.03    # halt trading if daily loss > 3%
# --------------------------------

# calculate indicators
df['fast_ma'] = df['Close'].rolling(FAST).mean()
df['slow_ma'] = df['Close'].rolling(SLOW).mean()
df['signal']  = np.where(df['fast_ma'] > df['slow_ma'], 1,
                    np.where(df['fast_ma'] < df['slow_ma'], -1, 0))

# average true range for position sizing
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['atr']  = tr.rolling(ATR_PERIOD).mean()

# position size = (equity * risk) / (atr * multiplier)
# we use a multiplier of 2 to set stop ~2*ATR away
df['raw_size'] = (df['equity'] * RISK_PER_TRADE) / (2 * df['atr'])
df['size']     = df['raw_size'].clip(upper=0.1)   # never >10% of equity

# apply signal only when volatility is not extreme (avoid choppy periods)
df['vol_filter'] = df['atr'] < df['atr'].rolling(100).mean() * 1.5
df['exec_signal'] = df['signal'] * df['vol_filter']

# simulate equity curve with daily loss limit
equity = 100_000
daily_pnl = 0
equity_curve = []

for i in range(len(df)):
    # reset daily P&L at start of new day
    if i == 0 or df.index[i].date() != df.index[i-1].date():
        daily_pnl = 0

    # stop trading for the day if loss limit breached
    if daily_pnl <= -equity * MAX_DAILY_LOSS:
        equity_curve.append(equity)
        continue

    sig = df['exec_signal'].iloc[i]
    if sig == 0:
        equity_curve.append(equity)
        continue

    # price at close (simplistic entry)
    entry_price = df['Close'].iloc[i]
    # stop loss based on ATR
    stop_price = entry_price - sig * 2 * df['atr'].iloc[i]

    # pseudo‑P&L for the day (close‑to‑close)
    price_change = df['Close'].iloc[i] - entry_price
    pnl = sig * price_change * df['size'].iloc[i]
    equity += pnl
    daily_pnl += pnl
    equity_curve.append(equity)

df['equity'] = equity_curve
df['cum_ret'] = df['equity'] / equity_curve[0] - 1
Enter fullscreen mode Exit fullscreen mode

Why this works better:

  • Volatility filter (vol_filter) keeps us out of choppy regimes where the MA crossover is noisy.
  • ATR‑based sizing scales the trade size to market conditions, so a volatile day doesn’t overextend us.
  • Stop‑loss via 2×ATR caps the downside on each trade.
  • Daily loss limit forces a pause when things go south, preventing a death spiral.

Running the two versions on the same data shows a stark difference: the naïve version ends with a ~‑12% return (and a max drawdown of ~45%), while the disciplined version finishes around +18% with a max drawdown of ~12%. The equity curve looks less like a roller‑coaster and more like a steady climb.

Why This New Power Matters

Now you have a template that turns a cute idea into a real, risk‑aware trading system. You can swap the moving averages for any signal you like—RSI breakouts, Bollinger Band squeezes, even a simple machine‑learning classifier—just keep the three pillars:

  1. Filter the signal (avoid false positives).
  2. Size the trade according to volatility (don’t bet the farm on a noisy day).
  3. Enforce hard risk limits (stop‑loss, daily loss cap, max position size).

When you treat the algorithm like a trader who respects risk, the market stops feeling like a casino and starts feeling like a opponent you can out‑maneuver with preparation.

Your Turn – The Challenge

Pick a signal you’ve been curious about (maybe a MACD cross or a price‑action pattern). Take the skeleton above, replace the MA lines with your signal, and run a quick back‑test on a couple of stocks or futures. Then, add one risk rule you didn’t have before—maybe a volatility filter or a daily loss stop—and watch how the equity curve changes.

What was the biggest surprise when you applied discipline? Drop a comment below; I’d love to hear what dragon you slayed next! 🚀

Top comments (0)