Spring Reversal is a Wyckoff concept where price briefly breaks support and then springs back above it.
This shakeout traps late sellers and often marks the beginning of an uptrend.
Testing indicates an average annual return of about 55%. It performs best in the stocks market.
The strategy buys once price reclaims the broken level, anticipating swift short covering and new demand.
A stop just below the spring low limits downside, and the position is closed if follow-through fails.
Details
Entry Criteria: indicator signal
Long/Short: Both
Exit Criteria: stop-loss or opposite signal
Stops: Yes, percent based
Default Values:
CandleType = 15 minute
LookbackPeriod = 20
StopLossPercent = 2
Support is the lowest low of the previous LookbackPeriod candles. The strategy only buys: a bullish candle that breaks below support and closes back above it. The stop lies StopLossPercent below the spring low and the position closes when a candle closes below it.
Filters:
Category: Reversal
Direction: Both
Indicators: Wyckoff
Stops: Yes
Complexity: Intermediate
Timeframe: Intraday
Seasonality: No
Neural networks: No
Divergence: No
Risk level: Medium
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Spring Reversal strategy.
/// Support is the lowest low of the previous LookbackPeriod candles. A bullish candle that breaks below support and closes back above it
/// is a spring and buys while flat. The stop lies StopLossPercent below the spring low, and a close below it closes the position.
/// </summary>
public class SpringReversalStrategy : Strategy
{
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly List<(decimal High, decimal Low)> _candles = [];
private decimal _stopPrice;
/// <summary>
/// Number of previous candles that form the level.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Distance of the stop beyond the level, in percent.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public SpringReversalStrategy()
{
_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("LookbackPeriod", "Number of previous candles that form the level", "Pattern");
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetNotNegative()
.SetDisplay("Stop Loss %", "Distance of the stop beyond the level, in percent", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_candles.Clear();
_stopPrice = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_candles.Clear();
_stopPrice = default;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
// The level is formed by the candles before this one.
var ready = _candles.Count == LookbackPeriod;
var high = ready ? _candles.Max(c => c.High) : 0m;
var low = ready ? _candles.Min(c => c.Low) : 0m;
_candles.Add((candle.HighPrice, candle.LowPrice));
if (_candles.Count > LookbackPeriod)
_candles.RemoveAt(0);
if (!IsFormedAndOnlineAndAllowTrading())
return;
var close = candle.ClosePrice;
if (Position > 0)
{
if (close <= _stopPrice)
SellMarket(Position);
return;
}
if (Position != 0 || !ready || !(candle.LowPrice < low && close > low && close > candle.OpenPrice))
return;
BuyMarket(Volume);
_stopPrice = candle.LowPrice * (1 - StopLossPercent / 100m);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class spring_reversal_strategy(Strategy):
"""
Spring Reversal strategy.
Support is the lowest low of the previous LookbackPeriod candles. A bullish candle that breaks below support and closes back above it
is a spring and buys while flat. The stop lies StopLossPercent below the spring low, and a close below it closes the position.
"""
def __init__(self):
super(spring_reversal_strategy, self).__init__()
self._lookback_period = self.Param("LookbackPeriod", 20).SetGreaterThanZero().SetDisplay("LookbackPeriod", "Number of previous candles that form the level", "Pattern")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0).SetNotNegative().SetDisplay("Stop Loss %", "Distance of the stop beyond the level, in percent", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._candles = []
self._stop_price = Decimal(0)
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(spring_reversal_strategy, self).OnReseted()
self._candles = []
self._stop_price = Decimal(0)
def OnStarted2(self, time):
super(spring_reversal_strategy, self).OnStarted2(time)
self._candles = []
self._stop_price = Decimal(0)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
# The level is formed by the candles before this one.
period = self._lookback_period.Value
ready = len(self._candles) == period
high = max(c[0] for c in self._candles) if ready else Decimal(0)
low = min(c[1] for c in self._candles) if ready else Decimal(0)
self._candles.append((candle.HighPrice, candle.LowPrice))
if len(self._candles) > period:
self._candles.pop(0)
if not self.IsFormedAndOnlineAndAllowTrading():
return
close = candle.ClosePrice
if self.Position > 0:
if close <= self._stop_price:
self.SellMarket(self.Position)
return
if self.Position != 0 or not ready or not (candle.LowPrice < low and close > low and close > candle.OpenPrice):
return
percent = Decimal(self._stop_loss_percent.Value) / Decimal(100)
self.BuyMarket(self.Volume)
self._stop_price = candle.LowPrice * (Decimal(1) - percent)
def CreateClone(self):
return spring_reversal_strategy()