This strategy fits a polynomial regression line to recent prices and builds upper and lower bands from the standard deviation of residuals. Long positions are opened when price falls below the lower band and short positions are opened when price rises above the upper band.
Details
Entry Criteria:
Long: Close < LowerBand.
Short: Close > UpperBand.
Long/Short: Both sides.
Exit Criteria:
Opposite signal.
Stops: No.
Default Values:
Length = 100.
Degree = 2.
StdDevMultiplier = 2.
CandleType = TimeSpan.FromMinutes(5).TimeFrame().
Filters:
Category: Mean reversion
Direction: Both
Indicators: Polynomial regression
Stops: No
Complexity: Moderate
Timeframe: Medium-term
Seasonality: No
Neural networks: No
Divergence: No
Risk level: Medium
Calculation and execution
Each finished candle fits a least-squares polynomial of Degree to the latest Length closes, including the current close, on an equally spaced grid from -1 to 1. The channel center is the fitted value at the newest observation. Width is StdDevMultiplier * sqrt(sum((close[i] - fitted[i])^2) / Length): population deviation of residuals, with no degrees-of-freedom correction.
A close strictly below the lower band enters long; strictly above the upper band enters short. An opposite entry fully reverses with one market order of Volume + abs(Position); a flat entry orders Volume. Prices inside the bands keep the position. No trading before the full window, no cooldown or protective stop.
Supported settings: 0 <= Degree <= 6, Length >= Degree + 2, positive StdDevMultiplier. Degree zero is a constant fit. The fixed normalized grid uses a precomputed orthonormal polynomial basis with two orthogonalization passes; each window projects price offsets onto that basis in double precision. C# and Python use the same calculation; order quantities remain decimal.
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Mean reversion outside a polynomial regression endpoint and its residual deviation bands.
/// </summary>
public class PolynomialRegressionBandsChannelStrategy : Strategy
{
private readonly StrategyParam<int> _length;
private readonly StrategyParam<int> _degree;
private readonly StrategyParam<decimal> _stdDevMultiplier;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _prices = [];
private double[][] _basis;
/// <summary>
/// Initializes the published regression length, degree and band multiplier.
/// </summary>
public PolynomialRegressionBandsChannelStrategy()
{
_length = Param(nameof(Length), 100).SetGreaterThanZero()
.SetDisplay("Length", "Rolling regression window, including the current close", "Indicators");
_degree = Param(nameof(Degree), 2).SetRange(0, 6)
.SetDisplay("Degree", "Polynomial degree from zero through six", "Indicators");
_stdDevMultiplier = Param(nameof(StdDevMultiplier), 2m).SetGreaterThanZero()
.SetDisplay("Std Dev Multiplier", "Population deviation multiplier of fitted residuals", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of finished candles", "General");
}
/// <summary>
/// Number of finished closes in the regression window.
/// </summary>
public int Length { get => _length.Value; set => _length.Value = value; }
/// <summary>
/// Polynomial degree from zero through six; the window needs at least two more observations.
/// </summary>
public int Degree { get => _degree.Value; set => _degree.Value = value; }
/// <summary>
/// Positive multiplier of the population residual deviation.
/// </summary>
public decimal StdDevMultiplier { get => _stdDevMultiplier.Value; set => _stdDevMultiplier.Value = value; }
/// <summary>
/// Candle type; five minutes by default.
/// </summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prices.Clear();
_basis = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (Degree < 0 || Degree > 6 || Length < Degree + 2)
throw new ArgumentException("Degree must be between zero and six and Length at least Degree + 2.");
_prices.Clear();
_basis = BuildBasis(Length, Degree);
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private static double[][] BuildBasis(int length, int degree)
{
// The fixed grid permits one QR basis for every rolling window.
var basis = new double[degree + 1][];
for (var column = 0; column <= degree; column++)
{
var values = new double[length];
for (var row = 0; row < length; row++)
values[row] = Math.Pow(2.0 * row / (length - 1) - 1.0, column);
for (var pass = 0; pass < 2; pass++)
{
for (var previous = 0; previous < column; previous++)
{
var projection = 0.0;
for (var row = 0; row < length; row++)
projection += values[row] * basis[previous][row];
for (var row = 0; row < length; row++)
values[row] -= projection * basis[previous][row];
}
}
var squaredNorm = 0.0;
foreach (var value in values)
squaredNorm += value * value;
var norm = Math.Sqrt(squaredNorm);
for (var row = 0; row < length; row++)
values[row] /= norm;
basis[column] = values;
}
return basis;
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_prices.Add(candle.ClosePrice);
if (_prices.Count > Length)
_prices.RemoveAt(0);
if (_prices.Count < Length || !IsFormedAndOnlineAndAllowTrading())
return;
var values = new double[Length];
for (var row = 0; row < Length; row++)
values[row] = (double)(_prices[row] - _prices[0]);
var coefficients = new double[_basis.Length];
for (var column = 0; column < _basis.Length; column++)
{
for (var row = 0; row < Length; row++)
coefficients[column] += values[row] * _basis[column][row];
}
var squaredError = 0.0;
var center = 0.0;
for (var row = 0; row < Length; row++)
{
var fitted = 0.0;
for (var column = 0; column < _basis.Length; column++)
fitted += coefficients[column] * _basis[column][row];
var residual = values[row] - fitted;
squaredError += residual * residual;
if (row == Length - 1)
center = fitted;
}
var width = (double)StdDevMultiplier * Math.Sqrt(squaredError / Length);
if (values[^1] < center - width && Position <= 0m)
BuyMarket(Volume + Math.Abs(Position));
else if (values[^1] > center + width && Position >= 0m)
SellMarket(Volume + Math.Abs(Position));
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import ArgumentException, Decimal, Math, TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class polynomial_regression_bands_channel_strategy(Strategy):
def __init__(self):
super(polynomial_regression_bands_channel_strategy, self).__init__()
self._length = self.Param("Length", 100).SetGreaterThanZero() \
.SetDisplay("Length", "Rolling regression window, including the current close", "Indicators")
self._degree = self.Param("Degree", 2).SetRange(0, 6) \
.SetDisplay("Degree", "Polynomial degree from zero through six", "Indicators")
self._std_dev_multiplier = self.Param("StdDevMultiplier", Decimal.Parse("2")).SetGreaterThanZero() \
.SetDisplay("Std Dev Multiplier", "Population deviation multiplier of fitted residuals", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of finished candles", "General")
self._prices = []
self._basis = None
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(polynomial_regression_bands_channel_strategy, self).OnReseted()
self._prices = []
self._basis = None
def OnStarted2(self, time):
super(polynomial_regression_bands_channel_strategy, self).OnStarted2(time)
length = self._length.Value
degree = self._degree.Value
if degree < 0 or degree > 6 or length < degree + 2:
raise ArgumentException("Degree must be between zero and six and Length at least Degree + 2.")
self._prices = []
self._basis = self._build_basis(length, degree)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
@staticmethod
def _build_basis(length, degree):
basis = []
for column in range(degree + 1):
values = [Math.Pow(2.0 * row / (length - 1) - 1.0, column) for row in range(length)]
for unused in range(2):
for previous in basis:
projection = sum(value * previous[row] for row, value in enumerate(values))
values = [value - projection * previous[row] for row, value in enumerate(values)]
norm = Math.Sqrt(sum(value * value for value in values))
basis.append([value / norm for value in values])
return basis
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
self._prices.append(candle.ClosePrice)
length = self._length.Value
if len(self._prices) > length:
self._prices.pop(0)
if len(self._prices) < length or not self.IsFormedAndOnlineAndAllowTrading():
return
origin = self._prices[0]
values = [float(price - origin) for price in self._prices]
coefficients = [sum(value * column[row] for row, value in enumerate(values)) for column in self._basis]
squared_error = 0.0
center = 0.0
for row, value in enumerate(values):
fitted = sum(coefficients[column] * vector[row] for column, vector in enumerate(self._basis))
residual = value - fitted
squared_error += residual * residual
if row == length - 1:
center = fitted
width = float(self._std_dev_multiplier.Value) * Math.Sqrt(squared_error / length)
if values[-1] < center - width and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif values[-1] > center + width and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
def CreateClone(self):
return polynomial_regression_bands_channel_strategy()