Trend Following: From the Golden Cross to Hidden Markov Models¶
Strategy Compendium · No. 01 · Category
trend_following(340 strategies) · 2026-09-02
If quantitative strategies have a family tree, its first page belongs to the moving-average crossover. It is the first “technical analysis” most traders ever meet: fast line crosses above slow line, buy; crosses below, sell. And because it is so simple, it is also the most underestimated family in the shop — within this repository’s trend_following category (340 strategies), crossovers and their close relatives alone occupy roughly 69 seats.
Here is a counterintuitive fact to set the tone: on gold’s 2008-2025 bull run, a bare 50/200 golden-cross system traded only 13 times in 18 years, won fewer than a third of those trades, and still turned 1,000,000 into 3,571,828. Win rate and profit are different variables — that is lesson one of trend following.
This digest tours the category through three of its highlights: the patient Golden Cross, the full Original Turtle Rules (position engineering, not just signals), and a Hidden Markov Model that turns “what regime are we in” into a computable quantity. Each is a self-contained backtest you can reproduce with one command.
Category at a Glance¶
Strategy |
Data |
Core idea |
Source |
|---|---|---|---|
Golden Cross |
XAUUSD daily, 2008-2025 |
50 SMA crosses above 200 SMA to enter; death cross exits |
|
SMA trend following |
XAUUSD daily |
Hold while price closes above the 200 SMA |
|
Original Turtle Rules |
XAUUSD M15 |
20/55-channel entries + ATR unit sizing + pyramiding |
|
Donchian color system |
XAUUSD M15→H4 |
Dual-timeframe channel “color” state machine |
|
MACD Sample (MT5 official) |
XAUUSD M15 |
Golden cross below zero + EMA26 slope confirmation |
|
ADX + MA |
XAUUSD M15 |
ADX threshold gates every MA-cross signal |
|
SuperTrend (Kolier) |
XAUUSD M15 |
ATR band flip = stop and reverse |
|
Woodies CCI |
XAUUSD M15→H4 |
Fast/slow CCI cloud transition |
|
Gold HMM trend following |
XAUUSD daily, 2024-2025 |
Gaussian HMM regimes with confidence gating |
|
Risk parity + trend gate |
Gold/silver/JPY/CHF/IEF daily |
Inverse-volatility weights, 200-day MA gate |
|
Deep Dive 1: Golden Cross — 13 Trades in 18 Years¶
Statistically, a golden cross is a crossing test between two sample means: the 50-day average is a proxy for recent momentum, the 200-day for the long-run baseline. The signal is sparse, lagging, and very quiet.
The implementation (test_0175) precomputes signals in pandas and keeps the strategy side dumb:
out['ma_fast'] = out['close'].rolling(window=fast_period).mean() # fast = 50
out['ma_slow'] = out['close'].rolling(window=slow_period).mean() # slow = 200
out['golden_cross'] = ((out['ma_fast'].shift(1) <= out['ma_slow'].shift(1)) &
(out['ma_fast'] > out['ma_slow'])).astype(float)
out['death_cross'] = ((out['ma_fast'].shift(1) >= out['ma_slow'].shift(1)) &
(out['ma_fast'] < out['ma_slow'])).astype(float)
def next(self):
golden_cross = float(self.data.golden_cross[0]) > 0.5
death_cross = float(self.data.death_cross[0]) > 0.5
if not self.position:
if golden_cross:
self.pending_order = self.buy(size=self._get_position_size(
target_notional_pct=float(self.p.lot_size)))
return
if death_cross:
self.pending_order = self.close()
Note the shift(1): the cross must compare the previous bar’s averages, so a signal cannot retro-fit itself on the current bar.
The pinned baseline. XAUUSD daily 2008-2025, 1,000,000 initial, 0.02% commission: 13 trades, 4 wins and 8 losses (one open), a 30.77% win rate, final value 3,571,828.03 (+257.18%), profit factor 2.04, max drawdown 37.54%. The test pins every number with tolerances like abs(final_value - 3571828.03) < 3.6. A 31% win rate making 2.5x on the back of a 2:1 payoff profile is trend following in one sentence: cut losses, let winners run.
The neighboring control group sharpens the point. The price-crossing variant (test_0001) trades 65 times (16.92% win rate) for a similar 3,686,124.79 — five times the turnover for the same money. And the death-cross-reverse strategy (test_0174) bets on the opposite side of the same signal: 12 trades, 75% win rate, +28.41%. Same indicator, three coherent uses.
Deep Dive 2: Original Turtle Rules — the Signal Is 10% of the System¶
The minimal Turtle rule fits in one line (see No. 15 for the Donchian minimal version). The full rulebook Richard Dennis handed his students is mostly position engineering: ATR-sized units, pyramiding every 1×ATR of favorable movement, a 4-unit cap. test_0074 ports all of it — n_st=20 (system-one channel), n_lt=55 (backup channel after a failed breakout), n_exit=10, atr_period=20, max_risk=0.01.
The soul of the system is the unit size — each unit risks only 1% of equity, converted to lots through ATR:
def _unit_size(self):
atr = float(self.atr[-1]) if len(self) > 1 else float(self.atr[0])
if atr <= 0:
return self.p.volume_min
equity = self.broker.getvalue()
risk_budget = equity * self.p.max_risk # 1% of equity per unit
unit = risk_budget / max(atr * self.p.stop_loss * self.p.multiplier, 1e-9)
return self._round_volume(unit)
Entries fire on a 20-day channel break (a 55-day backup re-confirmation follows a failed breakout); stops sit 1×ATR from entry; and each new unit of profit adds another unit:
st_upper = self._channel_max(self.p.n_st)
st_breakout = self._breakout(close, st_upper, st_lower)
if st_breakout == 0:
return
unit = self._unit_size()
self._set_risk_prices(st_breakout, close) # stop = entry ∓ 1×ATR
self.entry_order = self.buy(size=unit) if st_breakout > 0 else self.sell(size=unit)
# inside the position manager: add a unit every adding_interval × ATR of profit
if (close - self.last_entry_price) * current_direction > self.p.adding_interval * atr:
self.entry_order = self.buy(size=unit) if current_direction > 0 else self.sell(size=unit)
Baseline on three months of XAUUSD M15: 6,109 bars, 345 trades, 173 wins vs 172 losses (50.14% win rate), final value 1,190,431.17 (+19.04%), profit factor 1.23, max drawdown just 8.08%. A coin-flip win rate that still compounds — the profit lives entirely in the position structure: add into trends, stop at the start of them.
Deep Dive 3: Gold HMM — Making “Regime” Computable¶
“Is this a bull market?” Humans answer with feel; a hidden Markov model answers with posterior probabilities. test_0002 is a hand-written 431-line test that rolls a 3-state GaussianHMM(covariance_type="full") over gold daily bars — retrained every 21 days on a 252-day window, using just two features: log returns and 20-day annualized volatility. Raw states are then labeled BULL/BEAR/NEUTRAL by their mean return on the training set.
A state alone is not enough; the model must also be confident:
vol_factor = min(target_volatility / max(float(current_row["volatility_20"].iloc[0]), 1e-6),
max_target_percent / max(base_target_percent, 1e-6))
dynamic_target = min(max_target_percent, base_target_percent * current_confidence * vol_factor)
if current_confidence < state_persistence_min or persistence < state_persistence_min or consistent < 0.5:
dynamic_target = 0.0
Target exposure is min(0.10, 0.03 × confidence × volatility factor), and any of three trust checks — state posterior, transition-matrix stickiness, three consecutive same-state days — falling below 0.7 zeroes the position. Once unrealized profit reaches 8%, the stop ratchets from −3% to break-even. Result over 2024-2025: 245 daily bars, just 6 trades (3 wins, 3 losses), final value 1,001,059.99 — roughly flat after commissions. Two engineering habits worth stealing: pytest.importorskip("hmmlearn") degrades gracefully when the ML dependency is absent, and HMM features are precomputed in pandas, keeping the backtest engine itself pure.
The Rest of the Bench¶
MACD, three fates (
test_0116/test_0163/test_30): the MT5 official template with a zero-axis filter loses gently (-0.19%, PF 0.60); the naked stop-and-reverse crossover bleeds slower (-0.72% over 474 trades); and the MACD+KDJ combo with all-in sizing turns 100,000 into 5,870.49 — a 98.63% drawdown preserved forever as a lesson: sizing is the strategy.ADX gates and trailing stops (
test_0064,test_0140): the ADX+MA gate wins 45.5% and loses money; the ATR chandelier wins only 35.5% yet profits (PF 1.117, Sharpe 4.40). Two baselines, one verdict on win rate vs payoff.Woodies CCI (
test_0082): the community-evolved CCI cloud is the risk-adjusted standout of the confirmation family — PF 1.274, Sharpe 5.34, max drawdown 0.077%.Risk parity + 200-day gate (
test_0003): five safe-haven assets, monthly inverse-volatility weights, trend-gated to cash — 18 years, 206 rebalances, a 26% win rate, +23.6%.Dual-timeframe architecture (
test_0078): signals on a resampled H4 stream, orders on M15 — the standard skeleton for half the category’s MT5 ports.
Run It Yourself¶
# The whole category (300+ strategies, runonce/runnext parity asserted automatically)
pytest tests/functional/strategies/trend_following/ -v
# Just the Golden Cross
pytest tests/functional/strategies/trend_following/test_0175_golden_cross.py -v
Why Study Trend Following Here¶
Trend systems live on parameter sweeps — MA periods, channel lengths, ATR multipliers, pyramid intervals — and every knob changes the trade distribution. That is exactly what cloudQuant/backtrader is built for: 46% faster than the original in pure Python (all 1,152 strategy regressions finish in minutes), a median 128x speedup with the C++ backend (pip install back-trader-cpp) that turns a Turtle-parameter grid into a coffee break, runonce/runnext dual-mode parity so vectorized and event-driven engines must agree, and asserted metric baselines so you optimize the strategy — not chase the engine’s numerical drift.
Find it useful? Star the project on GitHub. Start from the series overview for the full map. A deeper (Chinese) treatment lives here, here, and here.
Risk disclaimer: for education and research only. Backtests use historical data and do not constitute investment advice; algorithmic trading carries substantial risk of loss.