Trendline Bounce Strategy
Markets often respect trendlines drawn across prior swing highs or lows. This strategy automatically fits regression lines to recent price action and looks for candles that bounce from those lines in the direction of the dominant trend.
Testing indicates an average annual return of about 124%. It performs best in the forex market.
Recent candles are stored to calculate upward or downward sloping support and resistance lines. When price nears a trendline and a candle confirms the bounce while staying on the correct side of a moving average, the system enters a trade. The stop is set using a percentage of price and an exit occurs on a cross of the moving average.
By only trading in the prevailing direction and waiting for a clear reaction at support or resistance, the method attempts to capture continuation moves without chasing breakouts.
Details
- Entry Criteria: Price touches calculated trendline and candle closes in trend direction above/below MA.
- Long/Short: Both.
- Exit Criteria: Price crossing moving average or stop-loss.
- Stops: Yes, percentage based.
- Default Values:
TrendlinePeriod= 20MAPeriod= 20BounceThresholdPercent= 0.5 A candle counts as touching a line when its low (high) comes within this many percent of the support (resistance) line.CandleType= 5 minuteStopLossPercent= 2
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: MA, Trendlines
- Stops: Yes
- Complexity: Advanced
- Timeframe: Intraday
- Seasonality: No
- Neural networks: No
- Divergence: No
- Risk level: Medium
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Trendline Bounce strategy.
/// Support is the regression line of the lows of the previous TrendlinePeriod candles and resistance that of their highs,
/// both extended to the current candle. While flat, a bullish candle whose low comes within BounceThresholdPercent of a rising
/// support and that closes above the moving average buys; a bearish candle at a falling resistance closing below it sells.
/// A cross of the moving average or a percent stop closes the position.
/// </summary>
public class TrendlineBounceStrategy : Strategy
{
private readonly StrategyParam<int> _trendlinePeriod;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<decimal> _bounceThresholdPercent;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _highs = [];
private readonly List<decimal> _lows = [];
/// <summary>
/// Number of previous candles the trendlines are fitted to.
/// </summary>
public int TrendlinePeriod
{
get => _trendlinePeriod.Value;
set => _trendlinePeriod.Value = value;
}
/// <summary>
/// Moving average period.
/// </summary>
public int MAPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// How close to a trendline the candle must come, in percent of the line.
/// </summary>
public decimal BounceThresholdPercent
{
get => _bounceThresholdPercent.Value;
set => _bounceThresholdPercent.Value = value;
}
/// <summary>
/// Stop-loss percentage.
/// </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 TrendlineBounceStrategy()
{
_trendlinePeriod = Param(nameof(TrendlinePeriod), 20)
.SetRange(2, 1000)
.SetDisplay("Trendline Period", "Previous candles the trendlines are fitted to", "Indicators");
_maPeriod = Param(nameof(MAPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Period for SMA", "Indicators");
_bounceThresholdPercent = Param(nameof(BounceThresholdPercent), 0.5m)
.SetNotNegative()
.SetDisplay("Bounce Threshold %", "How close to a trendline the candle must come", "Indicators");
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetNotNegative()
.SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).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();
_highs.Clear();
_lows.Clear();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_highs.Clear();
_lows.Clear();
var sma = new SimpleMovingAverage { Length = MAPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(sma, ProcessCandle)
.Start();
StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
// The stop has to see prices between candles, not only at their close.
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// The high-level handler activates native protection before this callback, also between signal bars.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue smaValue)
{
if (candle.State != CandleStates.Finished)
return;
// The lines are fitted to the candles before this one.
var ready = _highs.Count == TrendlinePeriod;
var (supportSlope, support) = ready ? FitAndExtend(_lows) : default;
var (resistanceSlope, resistance) = ready ? FitAndExtend(_highs) : default;
_highs.Add(candle.HighPrice);
_lows.Add(candle.LowPrice);
if (_highs.Count > TrendlinePeriod)
{
_highs.RemoveAt(0);
_lows.RemoveAt(0);
}
if (!ready || !smaValue.IsFormed || !IsFormedAndOnlineAndAllowTrading())
return;
var ma = smaValue.GetValue<decimal>();
var close = candle.ClosePrice;
if (Position > 0)
{
if (close < ma)
SellMarket(Position);
return;
}
if (Position < 0)
{
if (close > ma)
BuyMarket(-Position);
return;
}
var threshold = BounceThresholdPercent / 100m;
if (supportSlope > 0 && candle.LowPrice <= support * (1 + threshold) && close > candle.OpenPrice && close > ma)
BuyMarket(Volume);
else if (resistanceSlope < 0 && candle.HighPrice >= resistance * (1 - threshold) && close < candle.OpenPrice && close < ma)
SellMarket(Volume);
}
private static (decimal Slope, decimal Next) FitAndExtend(List<decimal> values)
{
// Least squares over x = 0..n-1, extended to x = n.
var n = values.Count;
decimal sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
for (var i = 0; i < n; i++)
{
sumX += i;
sumY += values[i];
sumXY += i * values[i];
sumX2 += i * i;
}
var slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
var intercept = (sumY - slope * sumX) / n;
return (slope, intercept + slope * n);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes, Level1Fields
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class trendline_bounce_strategy(Strategy):
"""
Trendline Bounce strategy.
Support is the regression line of the lows of the previous TrendlinePeriod candles and resistance that of their highs,
both extended to the current candle. While flat, a bullish candle whose low comes within BounceThresholdPercent of a rising
support and that closes above the moving average buys; a bearish candle at a falling resistance closing below it sells.
A cross of the moving average or a percent stop closes the position.
"""
def __init__(self):
super(trendline_bounce_strategy, self).__init__()
self._trendline_period = self.Param("TrendlinePeriod", 20).SetRange(2, 1000).SetDisplay("Trendline Period", "Previous candles the trendlines are fitted to", "Indicators")
self._ma_period = self.Param("MAPeriod", 20).SetGreaterThanZero().SetDisplay("MA Period", "Period for SMA", "Indicators")
self._bounce_threshold_percent = self.Param("BounceThresholdPercent", 0.5).SetNotNegative().SetDisplay("Bounce Threshold %", "How close to a trendline the candle must come", "Indicators")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0).SetNotNegative().SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._highs = []
self._lows = []
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(trendline_bounce_strategy, self).OnReseted()
self._highs = []
self._lows = []
def OnStarted2(self, time):
super(trendline_bounce_strategy, self).OnStarted2(time)
self._highs = []
self._lows = []
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(sma, self._process_candle).Start()
self.StartProtection(Unit(), Unit(Decimal(self._stop_loss_percent.Value), UnitTypes.Percent), useMarketOrders=True, isLocalStop=True)
# The stop has to see prices between candles, not only at their close.
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# The high-level handler activates native protection before the callback, including between bars.
pass
def _process_candle(self, candle, sma_value):
if candle.State != CandleStates.Finished:
return
# The lines are fitted to the candles before this one.
period = self._trendline_period.Value
ready = len(self._highs) == period
if ready:
support_slope, support = self._fit_and_extend(self._lows)
resistance_slope, resistance = self._fit_and_extend(self._highs)
self._highs.append(candle.HighPrice)
self._lows.append(candle.LowPrice)
if len(self._highs) > period:
self._highs.pop(0)
self._lows.pop(0)
if not ready or not sma_value.IsFormed or not self.IsFormedAndOnlineAndAllowTrading():
return
ma = sma_value.GetValue[Decimal](None)
close = candle.ClosePrice
if self.Position > 0:
if close < ma:
self.SellMarket(self.Position)
return
if self.Position < 0:
if close > ma:
self.BuyMarket(-self.Position)
return
threshold = Decimal(self._bounce_threshold_percent.Value) / Decimal(100)
if support_slope > 0 and candle.LowPrice <= support * (Decimal(1) + threshold) and close > candle.OpenPrice and close > ma:
self.BuyMarket(self.Volume)
elif resistance_slope < 0 and candle.HighPrice >= resistance * (Decimal(1) - threshold) and close < candle.OpenPrice and close < ma:
self.SellMarket(self.Volume)
@staticmethod
def _fit_and_extend(values):
# Least squares over x = 0..n-1, extended to x = n.
n = len(values)
sum_x = Decimal(0)
sum_y = Decimal(0)
sum_xy = Decimal(0)
sum_x2 = Decimal(0)
for i in range(n):
x = Decimal(i)
sum_x += x
sum_y += values[i]
sum_xy += x * values[i]
sum_x2 += x * x
count = Decimal(n)
slope = (count * sum_xy - sum_x * sum_y) / (count * sum_x2 - sum_x * sum_x)
intercept = (sum_y - slope * sum_x) / count
return slope, intercept + slope * count
def CreateClone(self):
return trendline_bounce_strategy()