在 GitHub 上查看

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));
	}
}