Trend & momentum strategies
The oldest edge in the book: markets that move tend to keep moving. Build a moving-average crossover and a breakout in pandas, see where they fire, and learn precisely when trend-following pays and when it bleeds.
Trend and momentum strategies bet on persistence: an instrument moving up is, on average, slightly more likely to keep moving up than a coin flip would suggest. The edge is behavioural and structural at once — investors chase performance, institutions accumulate over weeks, and risk models force more buying as volatility-adjusted trends strengthen. It is a small, unreliable edge on any single trade, but it shows up across centuries, asset classes, and timeframes, which is about as much as you can ask of any edge.
The moving-average crossover
The canonical trend rule uses two moving averages: a fast one that tracks recent price and a slow one that tracks the broader drift. When the fast crosses above the slow, recent momentum has turned up relative to the trend — go long. When it crosses below, go flat or short. The averages are just a way of asking "is the short-term picture stronger than the long-term picture?"
import numpy as np
import pandas as pd
def ma_crossover(df: pd.DataFrame, fast: int = class="n">20, slow: int = class="n">50) -> pd.DataFrame:
class="s">""class="s">"Long when fast MA is above slow MA, flat otherwise.
df needs a 'close' column. Returns df with signal + position columns."class="s">""
out = df.copy()
out[class="s">"ma_fast"] = out[class="s">"close"].rolling(fast).mean()
out[class="s">"ma_slow"] = out[class="s">"close"].rolling(slow).mean()
class="c"># State: class="n">1 while fast is above slow, else class="n">0
out[class="s">"signal"] = np.where(out[class="s">"ma_fast"] > out[class="s">"ma_slow"], class="n">1, class="n">0)
class="c"># Trade on the NEXT bar's open: you can only act after the cross prints.
class="c"># Shifting by class="n">1 avoids look-ahead bias — a cardinal sin (Part class="n">6).
out[class="s">"position"] = out[class="s">"signal"].shift(class="n">1).fillna(class="n">0)
class="c"># A class="s">"cross" is where the signal changes: +class="n">1 = entry, -class="n">1 = exit
out[class="s">"trade"] = out[class="s">"signal"].diff().fillna(class="n">0)
return out
Breakouts: trading the escape
A breakout takes the same "moves persist" idea but triggers on a level instead of a crossover. The classic is the *Donchian channel*: go long when price closes above the highest high of the last N bars, because escaping a range signals that a new trend is beginning and the old sellers have been exhausted. This is the logic behind the famous Turtle Traders system.
def donchian_breakout(df: pd.DataFrame, entry: int = class="n">20, exit: int = class="n">10) -> pd.DataFrame:
class="s">""class="s">"Long on a break of the N-bar high, exit on a break of the M-bar low.
Uses separate windows so exits are tighter than entries (Turtle-style)."class="s">""
out = df.copy()
class="c"># Prior-bar channels: .shift(class="n">1) so today's bar cannot see its own high
out[class="s">"upper"] = out[class="s">"high"].rolling(entry).max().shift(class="n">1)
out[class="s">"lower"] = out[class="s">"low"].rolling(exit).min().shift(class="n">1)
long_entry = out[class="s">"close"] > out[class="s">"upper"] class="c"># break the ceiling -> enter
long_exit = out[class="s">"close"] < out[class="s">"lower"] class="c"># break the floor -> leave
class="c"># Walk the state forward: hold the position until an exit fires.
position = np.zeros(len(out))
holding = class="n">0
for i in range(len(out)):
if holding == class="n">0 and long_entry.iloc[i]:
holding = class="n">1
elif holding == class="n">1 and long_exit.iloc[i]:
holding = class="n">0
position[i] = holding
out[class="s">"position"] = pd.Series(position, index=out.index).shift(class="n">1).fillna(class="n">0)
return out
When trend works — and when it hurts
Trend following has a very particular P&L signature, and you must know it before you trade it or you will abandon it at exactly the wrong moment. It wins rarely but big: a low win rate (often 35–45%), many small losses as it gets chopped in and out of ranges, and a handful of enormous winners that pay for everything. The equity curve is a long, frustrating grind punctuated by sharp rises.
- It works in markets that trend: instruments driven by slow fundamental shifts (commodities, rates, FX during policy divergence), higher timeframes, and volatile regimes where moves extend.
- It hurts in ranging, mean-reverting markets: quiet, low-volatility periods where price oscillates around a level. Every crossover is a whipsaw — you buy the top of the range and sell the bottom, paying the spread each time.
The mirror image of trend following is mean reversion — betting that moves *overshoot* and snap back. Next lesson we build it, and then confront the fact that these two philosophies want opposite things from the market.