AlgoPro UniversityCourse home
Part 4 · Lesson

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?"

pythonA moving-average crossover as a vectorised signal
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
Crossover on a trending instrument
Price (blue) with the 20-bar fast average (violet) and 50-bar slow average (green). The strategy is long while violet is above green — it catches the big sustained move and steps aside as the averages converge.

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.

pythonA Donchian-channel breakout with an ATR-based exit
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.

big losssmall lossscratchsmall winbig winhuge winThe trend-following P&L shape
Distribution of trade outcomes for a typical trend follower: lots of small losers, a few outsized winners. The right tail is the whole business — cut it off and the edge dies.

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.