Linear Regression Channel
Trades pullbacks when price moves outside a linear regression channel and reverts toward the mean.
Details
- Data: Price candles.
- Entry: Buy when close drops below lower band in an uptrend; sell when close rises above upper band in a downtrend.
- Exit: Close when price returns to the regression line.
- Instruments: Any instruments.
- Risk: Channel boundaries act as dynamic limits.
using System;
using System.Linq;
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>
/// Strategy trading pullbacks using a linear regression channel.
/// </summary>
public class LinearRegressionChannelStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _length;
private readonly StrategyParam<decimal> _deviation;
private readonly StrategyParam<int> _cooldownBars;
private Queue<decimal> _closes = new();
private int _barsFromSignal;
/// <summary>
/// Candle type for calculations.
/// </summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
/// <summary>
/// Number of bars used in regression.
/// </summary>
public int Length { get => _length.Value; set => _length.Value = value; }
/// <summary>
/// Channel width multiplier.
/// </summary>
public decimal Deviation { get => _deviation.Value; set => _deviation.Value = value; }
/// <summary>
/// Minimum bars between trade actions.
/// </summary>
public int CooldownBars { get => _cooldownBars.Value; set => _cooldownBars.Value = value; }
/// <summary>
/// Initializes a new instance of the <see cref="LinearRegressionChannelStrategy"/> class.
/// </summary>
public LinearRegressionChannelStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles", "General");
_length = Param(nameof(Length), 50)
.SetGreaterThanZero()
.SetDisplay("Length", "Bars for regression", "Parameters")
.SetOptimize(80, 240, 40);
_deviation = Param(nameof(Deviation), 1.5m)
.SetGreaterThanZero()
.SetDisplay("Deviation", "Channel width multiplier", "Parameters")
.SetOptimize(2m, 4m, 0.5m);
_cooldownBars = Param(nameof(CooldownBars), 20)
.SetGreaterThanZero()
.SetDisplay("Cooldown Bars", "Minimum bars between signals", "Parameters")
.SetOptimize(10, 40, 2);
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_closes = new Queue<decimal>();
_barsFromSignal = CooldownBars;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_barsFromSignal = CooldownBars;
var dummyEma1 = new ExponentialMovingAverage { Length = 10 };
var dummyEma2 = new ExponentialMovingAverage { Length = 20 };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(dummyEma1, dummyEma2, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal d1, decimal d2)
{
if (candle.State != CandleStates.Finished)
return;
_closes.Enqueue(candle.ClosePrice);
if (_closes.Count > Length)
_closes.Dequeue();
if (_closes.Count < Length)
return;
var closesSnapshot = _closes.ToArray();
var n = closesSnapshot.Length;
decimal sumX = 0;
decimal sumY = 0;
decimal sumXY = 0;
decimal sumX2 = 0;
var i = 0;
foreach (var y in closesSnapshot)
{
var x = (decimal)i;
sumX += x;
sumY += y;
sumXY += x * y;
sumX2 += x * x;
i++;
}
var denom = n * sumX2 - sumX * sumX;
if (denom == 0)
return;
var slope = (n * sumXY - sumX * sumY) / denom;
var intercept = (sumY - slope * sumX) / n;
var lastX = n - 1;
var line = intercept + slope * lastX;
decimal devSum = 0;
i = 0;
foreach (var y in closesSnapshot)
{
var x = (decimal)i;
var fitted = intercept + slope * x;
var diff = y - fitted;
devSum += diff * diff;
i++;
}
var deviation = (decimal)Math.Sqrt((double)(devSum / n));
var upper = line + deviation * Deviation;
var lower = line - deviation * Deviation;
_barsFromSignal++;
if (_barsFromSignal < CooldownBars)
return;
if (slope > 0 && candle.ClosePrice < lower && Position == 0)
{
BuyMarket();
_barsFromSignal = 0;
}
else if (slope < 0 && candle.ClosePrice > upper && Position == 0)
{
SellMarket();
_barsFromSignal = 0;
}
else if (Position > 0 && candle.ClosePrice >= line)
{
SellMarket();
_barsFromSignal = 0;
}
else if (Position < 0 && candle.ClosePrice <= line)
{
BuyMarket();
_barsFromSignal = 0;
}
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class linear_regression_channel_strategy(Strategy):
"""
Trades pullbacks using a linear regression channel.
Buys in uptrend when price dips below lower band, sells in downtrend above upper.
"""
def __init__(self):
super(linear_regression_channel_strategy, self).__init__()
self._length = self.Param("Length", 50) \
.SetDisplay("Length", "Bars for regression", "Parameters")
self._deviation = self.Param("Deviation", 1.5) \
.SetDisplay("Deviation", "Channel width multiplier", "Parameters")
self._cooldown_bars = self.Param("CooldownBars", 20) \
.SetDisplay("Cooldown Bars", "Min bars between signals", "Parameters")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles", "General")
self._closes = []
self._bars_from_signal = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(linear_regression_channel_strategy, self).OnReseted()
self._closes = []
self._bars_from_signal = self._cooldown_bars.Value
def OnStarted2(self, time):
super(linear_regression_channel_strategy, self).OnStarted2(time)
self._bars_from_signal = self._cooldown_bars.Value
self._closes = []
dummy_ema1 = ExponentialMovingAverage()
dummy_ema1.Length = 10
dummy_ema2 = ExponentialMovingAverage()
dummy_ema2.Length = 20
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(dummy_ema1, dummy_ema2, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle, d1, d2):
if candle.State != CandleStates.Finished:
return
length = self._length.Value
close = float(candle.ClosePrice)
self._closes.append(close)
if len(self._closes) > length:
self._closes.pop(0)
if len(self._closes) < length:
return
n = len(self._closes)
sum_x = 0.0
sum_y = 0.0
sum_xy = 0.0
sum_x2 = 0.0
for i in range(n):
x = float(i)
y = self._closes[i]
sum_x += x
sum_y += y
sum_xy += x * y
sum_x2 += x * x
denom = n * sum_x2 - sum_x * sum_x
if denom == 0:
return
slope = (n * sum_xy - sum_x * sum_y) / denom
intercept = (sum_y - slope * sum_x) / n
last_x = n - 1
line = intercept + slope * last_x
dev_sum = 0.0
for i in range(n):
fitted = intercept + slope * float(i)
diff = self._closes[i] - fitted
dev_sum += diff * diff
std_dev = math.sqrt(dev_sum / n)
mult = self._deviation.Value
upper = line + std_dev * mult
lower = line - std_dev * mult
self._bars_from_signal += 1
if self._bars_from_signal < self._cooldown_bars.Value:
return
if slope > 0 and close < lower and self.Position == 0:
self.BuyMarket()
self._bars_from_signal = 0
elif slope < 0 and close > upper and self.Position == 0:
self.SellMarket()
self._bars_from_signal = 0
elif self.Position > 0 and close >= line:
self.SellMarket()
self._bars_from_signal = 0
elif self.Position < 0 and close <= line:
self.BuyMarket()
self._bars_from_signal = 0
def CreateClone(self):
return linear_regression_channel_strategy()