Polynomial Regression Bands Channel 策略
该策略对最近价格进行多项式回归,并根据残差的标准差构建上下通道。当价格跌破下轨时开多头仓位,当价格突破上轨时开空头仓位。
细节
- 入场条件:
- 多头:
Close < LowerBand。 - 空头:
Close > UpperBand。
- 多头:
- 多空方向: 双向。
- 出场条件:
- 相反信号。
- 止损: 否。
- 默认值:
Length= 100。Degree= 2。StdDevMultiplier= 2。CandleType= TimeSpan.FromMinutes(5).TimeFrame().
- 筛选器:
- 类别: 均值回归
- 方向: 双向
- 指标: 多项式回归
- 止损: 否
- 复杂度: 中等
- 时间框架: 中期
- 季节性: 否
- 神经网络: 否
- 背离: 否
- 风险等级: 中等
计算与执行
每根已完成蜡烛在 -1 到 1 的等距网格上,对最近 Length 个收盘价(包含当前收盘价)进行 Degree 次多项式最小二乘拟合。通道中心为最新观测点的拟合值。宽度为 StdDevMultiplier * sqrt(sum((close[i] - fitted[i])^2) / Length),即残差的总体标准差,不做自由度修正。
收盘价严格低于下轨时做多,严格高于上轨时做空。反向入场以一笔数量为 Volume + abs(Position) 的市价订单完全反转仓位;空仓入场使用 Volume。价格位于通道内时保持仓位。窗口未满不交易,没有冷却时间或保护止损。
有效设置:0 <= Degree <= 6、Length >= Degree + 2、正的 StdDevMultiplier。零次拟合为常数。固定归一化网格预计算经两次正交化的标准正交多项式基;每个窗口以 double 精度投影价格偏移。C# 与 Python 使用相同计算,订单数量保持 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()