Four Bar Momentum Reversal Strategy
The Four Bar Momentum Reversal strategy enters long when the close has been below the close from Lookback bars ago for at least BuyThreshold consecutive candles within the selected time window. The position is closed once price breaks above the previous candle high.
Details
- Entry Criteria:
BuyThresholdconsecutive closes below the close fromLookbackbars ago inside the time window. - Exit Criteria: Close price greater than the previous candle high.
- Stops: None.
- Default Values:
BuyThreshold= 4Lookback= 4StartTime= 2014-01-01EndTime= 2099-01-01
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Four Bar Momentum Reversal Strategy.
/// Enters long after consecutive closes below the close from N bars ago.
/// Exits on breakout above previous high.
/// Uses EMA as trend filter.
/// </summary>
public class FourBarMomentumReversalStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _buyThreshold;
private readonly StrategyParam<int> _lookback;
private readonly StrategyParam<int> _emaLength;
private readonly StrategyParam<int> _cooldownBars;
private ExponentialMovingAverage _ema;
private readonly List<decimal> _closes = new();
private int _belowCount;
private decimal _prevHigh;
private int _cooldownRemaining;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int BuyThreshold { get => _buyThreshold.Value; set => _buyThreshold.Value = value; }
public int Lookback { get => _lookback.Value; set => _lookback.Value = value; }
public int EmaLength { get => _emaLength.Value; set => _emaLength.Value = value; }
public int CooldownBars { get => _cooldownBars.Value; set => _cooldownBars.Value = value; }
public FourBarMomentumReversalStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_buyThreshold = Param(nameof(BuyThreshold), 3)
.SetGreaterThanZero()
.SetDisplay("Buy Threshold", "Consecutive closes below reference to trigger buy", "Strategy");
_lookback = Param(nameof(Lookback), 4)
.SetGreaterThanZero()
.SetDisplay("Lookback", "Number of bars to compare", "Strategy");
_emaLength = Param(nameof(EmaLength), 20)
.SetGreaterThanZero()
.SetDisplay("EMA Length", "EMA trend filter period", "Indicators");
_cooldownBars = Param(nameof(CooldownBars), 15)
.SetDisplay("Cooldown Bars", "Bars between trades", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_ema = null;
_closes.Clear();
_belowCount = 0;
_prevHigh = 0;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ema = new ExponentialMovingAverage { Length = EmaLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_ema, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ema);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal emaVal)
{
if (candle.State != CandleStates.Finished)
return;
if (!_ema.IsFormed)
{
_closes.Add(candle.ClosePrice);
_prevHigh = candle.HighPrice;
return;
}
var close = candle.ClosePrice;
// Track past closes for lookback comparison
if (_closes.Count >= Lookback)
{
var pastClose = _closes[_closes.Count - Lookback];
if (close < pastClose)
_belowCount++;
else
_belowCount = 0;
}
_closes.Add(close);
// Keep list from growing too large
if (_closes.Count > Lookback + 10)
_closes.RemoveAt(0);
if (!IsFormedAndOnlineAndAllowTrading())
{
_prevHigh = candle.HighPrice;
return;
}
if (_cooldownRemaining > 0)
{
_cooldownRemaining--;
_prevHigh = candle.HighPrice;
return;
}
// Buy: consecutive closes below reference + price below EMA (reversal from weakness)
if (_belowCount >= BuyThreshold && Position <= 0)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(Volume);
_cooldownRemaining = CooldownBars;
}
// Exit long: breakout above previous high
else if (Position > 0 && close > _prevHigh)
{
SellMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
// Short: consecutive closes above reference (overbought reversal)
else if (_belowCount == 0 && _closes.Count > Lookback)
{
var pastClose = _closes[_closes.Count - 1 - Lookback];
var aboveCount = 0;
for (int i = _closes.Count - 1; i >= Math.Max(0, _closes.Count - BuyThreshold); i--)
{
if (_closes[i] > pastClose)
aboveCount++;
else
break;
}
if (aboveCount >= BuyThreshold && Position >= 0 && close > emaVal)
{
if (Position > 0)
SellMarket(Math.Abs(Position));
SellMarket(Volume);
_cooldownRemaining = CooldownBars;
}
}
_prevHigh = candle.HighPrice;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class four_bar_momentum_reversal_strategy(Strategy):
"""Four Bar Momentum Reversal Strategy."""
def __init__(self):
super(four_bar_momentum_reversal_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._buy_threshold = self.Param("BuyThreshold", 3) \
.SetDisplay("Buy Threshold", "Consecutive closes below reference to trigger buy", "Strategy")
self._lookback = self.Param("Lookback", 4) \
.SetDisplay("Lookback", "Number of bars to compare", "Strategy")
self._ema_length = self.Param("EmaLength", 20) \
.SetDisplay("EMA Length", "EMA trend filter period", "Indicators")
self._cooldown_bars = self.Param("CooldownBars", 15) \
.SetDisplay("Cooldown Bars", "Bars between trades", "Risk")
self._ema = None
self._closes = []
self._below_count = 0
self._prev_high = 0.0
self._cooldown_remaining = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(four_bar_momentum_reversal_strategy, self).OnReseted()
self._ema = None
self._closes = []
self._below_count = 0
self._prev_high = 0.0
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(four_bar_momentum_reversal_strategy, self).OnStarted2(time)
self._ema = ExponentialMovingAverage()
self._ema.Length = int(self._ema_length.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._ema, self._on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._ema)
self.DrawOwnTrades(area)
def _on_process(self, candle, ema_val):
if candle.State != CandleStates.Finished:
return
if not self._ema.IsFormed:
self._closes.append(float(candle.ClosePrice))
self._prev_high = float(candle.HighPrice)
return
close = float(candle.ClosePrice)
lookback = int(self._lookback.Value)
# Track past closes for lookback comparison
if len(self._closes) >= lookback:
past_close = self._closes[len(self._closes) - lookback]
if close < past_close:
self._below_count += 1
else:
self._below_count = 0
self._closes.append(close)
if len(self._closes) > lookback + 10:
self._closes.pop(0)
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_high = float(candle.HighPrice)
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
self._prev_high = float(candle.HighPrice)
return
buy_threshold = int(self._buy_threshold.Value)
cooldown = int(self._cooldown_bars.Value)
ema_v = float(ema_val)
# Buy: consecutive closes below reference
if self._below_count >= buy_threshold and self.Position <= 0:
if self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
self.BuyMarket(self.Volume)
self._cooldown_remaining = cooldown
# Exit long: breakout above previous high
elif self.Position > 0 and close > self._prev_high:
self.SellMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
# Short: consecutive closes above reference (overbought reversal)
elif self._below_count == 0 and len(self._closes) > lookback:
past_close = self._closes[len(self._closes) - 1 - lookback]
above_count = 0
start = len(self._closes) - 1
end = max(0, len(self._closes) - buy_threshold)
for i in range(start, end - 1, -1):
if self._closes[i] > past_close:
above_count += 1
else:
break
if above_count >= buy_threshold and self.Position >= 0 and close > ema_v:
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
self.SellMarket(self.Volume)
self._cooldown_remaining = cooldown
self._prev_high = float(candle.HighPrice)
def CreateClone(self):
return four_bar_momentum_reversal_strategy()