The Quest Begins (The "Why")
I still remember the first time I stared at a candlestick chart and thought, “If I could just teach a computer to buy low and sell high, I’d be sipping coffee on a beach while my code does the hard work.” Spoiler: the reality was a lot less glamorous. I spent weeks cobbling together a simple moving‑average crossover strategy, back‑tested it on a couple of years of data, and watched the equity curve dip lower than my motivation after a Monday morning stand‑up.
The problem wasn’t the idea—it was the execution. I was treating the algorithm like a magic spell: write a few lines, run it, and profit would appear. I ignored transaction costs, position sizing, and the nasty habit of over‑fitting to noise. Each tweak felt like I was pushing a boulder uphill, only to watch it roll back down. I needed a revelation, a solid insight that would turn the quest from a frustrating grind into a genuine adventure.
The Revelation (The Insight)
The breakthrough came when I stopped chasing the “perfect entry” and started thinking about risk first. I read a few trader forums, skimmed Trading for a Living by Dr. Alexander Elder (no, that’s not the pop‑culture bit—save that for later), and realized that profitable trading isn’t about predicting the future; it’s about managing the downside while letting winners run.
Two simple changes turned my losing bot into a modestly profitable one:
- Volatility‑based position sizing – instead of betting a fixed amount each trade, I scaled the size according to the recent Average True Range (ATR). When the market was calm, I took smaller bites; when it got wild, I reduced exposure.
- Hard stop‑loss and trailing profit target – every trade got an ATR‑based stop (e.g., 1.5 × ATR) and a trailing exit that locked in gains as the price moved in my favor.
Suddenly, the equity curve stopped looking like a roller coaster designed by a sadist and started showing a steady upward drift. It felt like finally beating the final boss in Dark Souls after countless tries—relief, excitement, and a sudden urge to share the loot.
Wielding the Power (Code & Examples)
Below is the before version—a naive SMA crossover that ignored risk. I’ll keep it short so you can see the exact pain points.
# BEFORE: Naïve SMA crossover (no risk management)
import pandas as pd
def naive_sma_strategy(df, fast=10, slow=30):
df = df.copy()
df['fast_ma'] = df['close'].rolling(fast).mean()
df['slow_ma'] = df['close'].rolling(slow).mean()
df['signal'] = 0
df.loc[df['fast_ma'] > df['slow_ma'], 'signal'] = 1 # go long
df.loc[df['fast_ma'] < df['slow_ma'], 'signal'] = -1 # go short
df['positions'] = df['signal'].diff() # entry/exit points
return df
What’s wrong?
- Fixed unit size – every signal triggers a 1‑lot trade, regardless of volatility.
- No stop‑loss – a bad trade can run against you until the opposite signal appears, eating huge chunks of capital.
-
Look‑ahead bias – the
signalcolumn is calculated with the same bar’s close, which in a live setting would require the future price.
Now here’s the after version, where we treat risk as the first class citizen. I’ve added comments to highlight the traps we avoided.
# AFTER: Volatility‑scaled SMA with ATR stops
import pandas as pd
import numpy as np
def risk_aware_sma_strategy(df, fast=10, slow=30, atr_len=14,
risk_per_trade=0.01, # 1 % of equity per trade
atr_multiplier=1.5):
df = df.copy()
# 1️⃣ Indicators
df['fast_ma'] = df['close'].rolling(fast).mean()
df['slow_ma'] = df['close'].rolling(slow).mean()
df['raw_signal'] = np.where(df['fast_ma'] > df['slow_ma'], 1,
np.where(df['fast_ma'] < df['slow_ma'], -1, 0))
df['signal'] = df['raw_signal'].replace(0, method='ffill').fillna(0)
# 2️⃣ ATR for volatility
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_len).mean()
# 3️⃣ Position sizing: risk = equity * risk_per_trade
# stop distance = atr_multiplier * atr
# units = (equity * risk_per_trade) / (stop distance * point_value)
# Here we assume point_value = 1 (e.g., FX pair) and equity = 1 for simplicity.
df['risk_amount'] = 1 * risk_per_trade # placeholder equity
df['stop_dist'] = atr_multiplier * df['atr']
df['units'] = np.where(df['stop_dist'] > 0,
df['risk_amount'] / df['stop_dist'],
0)
# 4️⃣ Apply stops & trailing target
df['entry_price'] = np.nan
df['stop_price'] = np.nan
df['trail_price'] = np.nan
df['position'] = 0
in_trade = False
for i in range(len(df)):
if not in_trade and df['signal'].iat[i] != 0:
# enter trade
in_trade = True
df.iat[i, df.columns.get_loc('entry_price')] = df['close'].iat[i]
df.iat[i, df.columns.get_loc('stop_price')] = (
df['close'].iat[i] - df['signal'].iat[i] * df['stop_dist'].iat[i]
)
df.iat[i, df.columns.get_loc('trail_price')] = df['close'].iat[i]
df.iat[i, df.columns.get_loc('position')] = (
df['signal'].iat[i] * df['units'].iat[i]
)
elif in_trade:
# update trailing stop for longs, reverse for shorts
if df['position'].iat[i-1] > 0: # long
df.iat[i, df.columns.get_loc('trail_price')] = max(
df['trail_price'].iat[i-1], df['close'].iat[i]
)
new_stop = df['trail_price'].iat[i-1] - df['stop_dist'].iat[i]
df.iat[i, df.columns.get_loc('stop_price')] = max(
df['stop_price'].iat[i-1], new_stop
)
# exit if price hits stop
if df['low'].iat[i] <= df['stop_price'].iat[i]:
in_trade = False
df.iat[i, df.columns.get_loc('position')] = 0
else:
df.iat[i, df.columns.get_loc('position')] = df['position'].iat[i-1]
else: # short
df.iat[i, df.columns.get_loc('trail_price')] = min(
df['trail_price'].iat[i-1], df['close'].iat[i]
)
new_stop = df['trail_price'].iat[i-1] + df['stop_dist'].iat[i]
df.iat[i, df.columns.get_loc('stop_price')] = min(
df['stop_price'].iat[i-1], new_stop
)
if df['high'].iat[i] >= df['stop_price'].iat[i]:
in_trade = False
df.iat[i, df.columns.get_loc('position')] = 0
else:
df.iat[i, df.columns.get_loc('position')] = df['position'].iat[i-1]
# carry forward flat position
if not in_trade:
df.iat[i, df.columns.get_loc('position')] = 0
df['returns'] = df['close'].pct_change() * df['position'].shift(1)
df['equity'] = (1 + df['returns']).cumprod()
return df
Why this works:
-
Volatility scaling (
units) ensures we risk the same % of equity on every trade, automatically shrinking size during turbulent periods. - ATR‑based stop gives the trade room to breathe but cuts losses quickly when the market moves against us.
- Trailing exit lets winners run while locking in profit, turning a choppy series of small gains into a smoother equity curve.
- We avoided the classic look‑ahead bias by calculating the signal from prior bars only (
shift(1)in the returns line).
Common Traps (the “bosses” to dodge)
- Over‑fitting to historical noise – tweaking parameters until the back‑test looks perfect but fails live. Fix: keep a separate out‑of‑sample window or use walk‑forward validation.
- Ignoring transaction costs – a strategy that looks great on paper can evaporate once you subtract spreads and commissions. Fix: bake a realistic cost per trade into the returns calculation.
Run both versions on the same data (e.g., hourly EUR/USD for 6 months) and you’ll see the naive strategy’s equity curve bounce around zero, while the risk‑aware version creeps upward with far fewer wild swings.
Why This New Power Matters
Now you have a template that treats risk as the first ingredient, not an afterthought. You can swap the SMA for any signal—RSI breakouts, machine‑learning classifiers, or even a simple sentiment score—and the risk layer will keep you from blowing up your account. The beauty is that the same code works across stocks, crypto, or futures; you just adjust the ATR length and risk‑per‑trade to match the instrument’s volatility.
Armed with this approach, you’re no longer gambling on a lucky entry; you’re building a repeatable edge that survives market regimes. It’s the difference between hoping for a jackpot and running a casino where the odds are subtly in your favor.
Your Turn – The Challenge
Take the skeleton above, plug in your favorite indicator, and run a walk‑forward test on a dataset you care about.
Top comments (0)