SMA Crossover Template
The SMA Crossover is the most widely taught trend-following strategy. It uses two Simple Moving Averages of different lengths — when the faster one crosses above the slower one, the strategy buys. When it crosses below, the strategy sells.
How the strategy works
A Simple Moving Average (SMA) calculates the average closing price over the last N bars. A short-period SMA reacts quickly to price changes, while a long-period SMA smooths out noise and follows the broader trend.
The crossover logic:
- Buy signal: The fast SMA rises above the slow SMA. This indicates short-term momentum is turning bullish and price may be entering an uptrend.
- Sell signal: The fast SMA drops below the slow SMA. This indicates short-term momentum is turning bearish and price may be entering a downtrend.
The strategy maintains a position at all times after the first signal — it is either long, short, or waiting for enough data to compute the moving averages.
Signal flow
- Collect closing prices bar by bar
- Wait until enough bars exist to calculate the slow SMA (default: 30 bars)
- On each new bar, compute both the fast SMA and the slow SMA
- If fast SMA > slow SMA and current position is not already long: buy (close any short position first)
- If fast SMA < slow SMA and current position is not already short: sell (close any long position first)
- Repeat until all bars are processed
Parameters
| Parameter | Default | Description |
|---|---|---|
FAST_PERIOD | 10 | Number of bars for the fast (short-term) moving average |
SLOW_PERIOD | 30 | Number of bars for the slow (long-term) moving average |
MAX_BARS | 0 | Maximum bars to process. 0 means process all available bars in the date range |
How to set parameters
- FAST_PERIOD must be less than SLOW_PERIOD. If they are equal, the strategy will never generate a signal.
- Common pairings: 5/20, 10/30, 10/50, 20/50, 50/200.
- Lower values produce more signals (more trades, more whipsaws). Higher values produce fewer signals (fewer trades, catches larger trends).
The Python code
Here is the complete strategy logic (excluding the shared header, setup, and cleanup code that TestMax auto-generates):
FAST_PERIOD = int(os.environ.get("FAST_PERIOD", "10"))SLOW_PERIOD = int(os.environ.get("SLOW_PERIOD", "30"))MAX_BARS = int(os.environ.get("MAX_BARS", "0")) or int(os.environ.get("TOTAL_BARS", "999999"))
# ... (backtest session setup is auto-generated) ...
# ─── SMA CROSSOVER STRATEGY ─────────────────────────────closes = []position = None # "LONG" or "SHORT" or Nonetrade_count = 0wins = 0entry_price = 0
def sma(data, period): """Simple Moving Average.""" if len(data) < period: return None return sum(data[-period:]) / period
print(f"[INFO] Strategy: SMA Crossover (fast={FAST_PERIOD}, slow={SLOW_PERIOD})")
try: for i in range(MAX_BARS): bar = get_next_bar() if bar is None: print("[INFO] No more bars available") break
closes.append(bar["c"]) read_speed() if STEP_DELAY > 0: time.sleep(STEP_DELAY)
# Need enough data for slow MA if len(closes) < SLOW_PERIOD: continue
fast_ma = sma(closes, FAST_PERIOD) slow_ma = sma(closes, SLOW_PERIOD) price = bar["c"]
# Buy signal: fast crosses above slow if fast_ma > slow_ma and position != "LONG": if position == "SHORT": # Close short + open long result = place_order(ACCOUNT_ID, CONTRACT_ID, 0, 2) pnl = entry_price - price if pnl > 0: wins += 1 trade_count += 1 else: result = place_order(ACCOUNT_ID, CONTRACT_ID, 0, 1) position = "LONG" entry_price = price trade_count += 1 print(f"[TRADE] BUY @ {price:.2f} | Fast MA: {fast_ma:.2f} > Slow MA: {slow_ma:.2f}")
# Sell signal: fast crosses below slow elif fast_ma < slow_ma and position != "SHORT": if position == "LONG": result = place_order(ACCOUNT_ID, CONTRACT_ID, 1, 2) pnl = price - entry_price if pnl > 0: wins += 1 trade_count += 1 else: result = place_order(ACCOUNT_ID, CONTRACT_ID, 1, 1) position = "SHORT" entry_price = price trade_count += 1 print(f"[TRADE] SELL @ {price:.2f} | Fast MA: {fast_ma:.2f} < Slow MA: {slow_ma:.2f}")Key code details
sma(data, period)— A helper function that computes the average of the lastperiodvalues. ReturnsNoneif there is not enough data yet.- Position flipping — When the strategy switches from long to short (or vice versa), it places an order for size 2 to close the existing position and open a new one in the opposite direction in a single order.
read_speed()— Called on every bar to pick up speed changes from the UI. This allows you to speed up or slow down the backtest mid-execution.STEP_DELAY— The delay between bars, controlled by the speed slider. At 1x speed, this is approximately 0.02 seconds per bar.
Example output
[INFO] Strategy: SMA Crossover (fast=10, slow=30)[INFO] Using pre-created session: NQ (CON.F.US.ENQ.H25)[INFO] Account: 12345, Contract: CON.F.US.ENQ.H25, Bars: 390────────────────────────────────────────────────────────────[TRADE] BUY @ 21503.25 | Fast MA: 21501.30 > Slow MA: 21500.85[TRADE] SELL @ 21498.50 | Fast MA: 21499.10 < Slow MA: 21500.20[TRADE] BUY @ 21510.75 | Fast MA: 21508.40 > Slow MA: 21505.60[TRADE] SELL @ 21525.00 | Fast MA: 21522.15 < Slow MA: 21523.80...────────────────────────────────────────────────────────────[DONE] Backtest Complete![DONE] Final Balance: $50,325.00[DONE] Net P&L: +$325.00[DONE] Total Trades: 14[DONE] Win Rate: 57.1%Tuning tips
Choosing the right periods
If you are using 1-second or 5-second bars:
- Try FAST_PERIOD=5, SLOW_PERIOD=15 for more reactive signals
- Expect many trades and frequent whipsaws
- Works best during high-volatility sessions (market open, major news)
- The strategy will be in and out of positions quickly
If you are using 1-minute bars:
- The default FAST_PERIOD=10, SLOW_PERIOD=30 works well
- Also try 10/50 for catching larger intraday swings
- Good balance between signal frequency and trend-following accuracy
If you are using 5-minute or 15-minute bars over multiple days:
- Try FAST_PERIOD=20, SLOW_PERIOD=50 or even 50/200
- Fewer trades, but each trade captures a larger move
- Less susceptible to noise and whipsaws
Common pitfalls
- Whipsaws in ranging markets. The SMA crossover performs poorly when price oscillates in a narrow range. The fast and slow averages will repeatedly cross back and forth, generating losing trades. Consider using a wider spread between periods (e.g., 10/50 instead of 10/30) to reduce false signals.
- Lag. Moving averages are lagging indicators — they react to price changes after they happen. By the time a crossover occurs, the trend may already be well underway. This means you will miss the first part of a move and may exit after it has partially reversed.
- Period relative to timeframe. A 30-period SMA means 30 seconds on 1-second bars but 2.5 hours on 5-minute bars. Always think about the real time duration your periods represent.
Ideas for improvement
Once you are comfortable with the basic SMA Crossover, consider these enhancements in a custom strategy:
- Add a trend filter. Only take long signals when price is above a 200-period SMA (or short signals below it).
- Add RSI confirmation. Require RSI to be above 50 for buys and below 50 for sells to reduce false signals.
- Use a stop-loss. Exit a trade if the loss exceeds a fixed point value instead of waiting for the next crossover.
- Position sizing. Instead of always trading 1 contract, vary the size based on account equity or the strength of the signal.
See Custom Strategies to learn how to implement these modifications.