MA Cross
The classic trend-following pattern. Long when a fast moving average crosses above a slow one; exit when it crosses back below. Two stop layers protect each trade.
class MACross(Strategy): """Trend following with stop loss and take profit.""" fast_period = Param(7, min=2, max=50) slow_period = Param(21, min=10, max=200) trade = Symbol() sizing = FixedCapital()
def execute(self): fast = SMA(self.fast_period, on=self.trade) slow = SMA(self.slow_period, on=self.trade)
self.stop_loss(2.0) self.take_profit(5.0)
if fast.crossed_above(slow) and self.trade.is_flat: self.buy(self.trade, reason="Bullish crossover")
if fast.crossed_below(slow) and self.trade.is_long: self.sell(self.trade, self.trade.position, reason="Bearish crossover")Walkthrough
Parameters
fast_period and slow_period define the two SMAs. Both have hard bounds — the optimizer will respect them. Defaults of 7 and 21 are common starting points for daily bars.
Indicators
Both SMAs are declared at the top of execute(). Even when the buy/sell branches don’t fire, the SMAs receive each bar’s close and stay in sync.
Exit rules
Two rules registered every bar:
stop_loss(2.0)— exit if drawdown ≥ 2% from entry.take_profit(5.0)— exit if gain ≥ 5% from entry.
The first to trigger wins. Both are checked tick-by-tick, not just at bar close.
Entry
crossed_above returns True for the single bar where fast transitions from ≤ slow to > slow. The is_flat guard prevents the strategy from re-entering when already long (which would silently no-op anyway, but the explicit check makes intent clear).
Exit
Same pattern in reverse — crossed_below for one bar, exit only if currently long. The exit verb requires the explicit quantity (self.trade.position).
What to tune
Run an optimization sweep. Three dimensions to play with:
fast_period— usually 3..15 for daily bars, 5..30 for intraday.slow_period— usually 10..50 for daily, 20..100 for intraday. Slow must be greater than fast.stop_loss(...)/take_profit(...)— currently hardcoded. Promote to Params for the optimizer:
sl_pct = Param(2.0, min=0.5, max=5.0)tp_pct = Param(5.0, min=1.0, max=20.0)Common modifications
Symmetric short side
class LongShortMACross(Strategy): """Golden-cross trend follower, both directions.""" fast_period = Param(7, min=2, max=50) slow_period = Param(21, min=10, max=200) trade = Symbol() sizing = FixedCapital()
def execute(self): fast = SMA(self.fast_period, on=self.trade) slow = SMA(self.slow_period, on=self.trade)
if fast.crossed_above(slow) and self.trade.is_flat: self.buy(self.trade, reason="Bullish crossover")
if fast.crossed_below(slow) and self.trade.is_flat: self.sell_short(self.trade, reason="Bearish crossover")
if fast.crossed_above(slow) and self.trade.is_short: self.buy_to_cover(self.trade, self.trade.position, reason="Cover short")
if fast.crossed_below(slow) and self.trade.is_long: self.sell(self.trade, self.trade.position, reason="Exit long")Note: in a flat → long → flat → short pattern, you’ll need two bars (one to exit, one to enter the opposite side) — positions don’t flip automatically.
EMA instead of SMA
Swap SMA for EMA to react faster:
fast = EMA(self.fast_period, on=self.trade)slow = EMA(self.slow_period, on=self.trade)Whipsaw will increase. Pair with a longer-period trend filter (Aroon, for instance) to reduce false crossovers.