在 GitHub 上查看

该策略通过对最近的蜡烛进行多项式回归来重建原始 MQL4 专家使用的 Center of Gravity 通道。回归中心来自最小二乘解的常数项,通道宽度则来自同一观察窗口内的收盘价标准差。下轨等于中心减去放大后的标准差,对应于源码中调用的 stdl 缓冲区。

运行时维护一个长度为 Bars Back 的收盘价滚动队列。每当形成一根完整蜡烛时,策略都会使用高斯消元法求解正常方程组,重新计算回归系数。这样无需保存完整历史即可得到与自定义指标相同的通道结构。如果矩阵病态,计算会被跳过,以防止产生不稳定的交易决策。

交易逻辑继承自原始专家:当当前蜡烛的最低价仍高于下轨(MQL 表达为 lowerBand < Low)时,策略将其视为向均值回归的信号。如果没有持有多头头寸,则平掉所有空头并按策略的 Volume 参数买入。最新的中心值以及上下轨通过只读属性提供,方便绘图或诊断。

风控参数是可选的。Stop Loss DistanceTake Profit Distance 使用价格单位表示。当设置为正值时,策略会记录多头的入场价格,并根据蜡烛极值判断是否触发止损或止盈。如果保持为零,持仓可由人工或外部模块管理。

参数

  • Candle Type – 用于回归计算的蜡烛时间框架。
  • Bars Back – 参与回归计算的历史蜡烛数量(默认 125)。
  • Polynomial Degree – 多项式回归的阶数,决定通道的弯曲程度(默认 2)。
  • Std Multiplier – 构建通道时应用于标准差的倍数(默认 1)。
  • Stop Loss Distance – 多头止损价格距离,0 表示禁用。
  • Take Profit Distance – 多头止盈价格距离,0 表示禁用。

该策略仅处理已完成的蜡烛,使用市价单入场,而且不会自动做空,因为原始脚本中的卖出分支已被注释掉。

using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Center of Gravity regression channel mean reversion strategy.
/// Approximates price with a polynomial regression and builds a standard deviation envelope.
/// Buys when price stays above the lower deviation band and optional stops manage risk.
/// </summary>
public class CenterOfGravityMeanReversionStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _barsBack;
	private readonly StrategyParam<int> _polynomialDegree;
	private readonly StrategyParam<decimal> _stdMultiplier;
	private readonly StrategyParam<decimal> _stopLossDistance;
	private readonly StrategyParam<decimal> _takeProfitDistance;

	private readonly Queue<decimal> _closes = new();

	private decimal? _entryPrice;
	private decimal? _currentLowerBand;
	private decimal? _currentUpperBand;
	private decimal? _currentCenter;

	/// <summary>
	/// Initializes a new instance of the <see cref="CenterOfGravityMeanReversionStrategy"/> class.
	/// </summary>
	public CenterOfGravityMeanReversionStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to build the regression channel", "General");

		_barsBack = Param(nameof(BarsBack), 125)
			.SetGreaterThanZero()
			.SetDisplay("Bars Back", "Number of historical bars used for regression", "Channel")
			
			.SetOptimize(50, 200, 25);

		_polynomialDegree = Param(nameof(PolynomialDegree), 2)
			.SetGreaterThanZero()
			.SetDisplay("Polynomial Degree", "Degree of polynomial regression", "Channel");

		_stdMultiplier = Param(nameof(StdMultiplier), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Std Multiplier", "Multiplier applied to close price standard deviation", "Channel");

		_stopLossDistance = Param(nameof(StopLossDistance), 0m)
			.SetNotNegative()
			.SetDisplay("Stop Loss Distance", "Optional stop loss distance in price units", "Risk");

		_takeProfitDistance = Param(nameof(TakeProfitDistance), 0m)
			.SetNotNegative()
			.SetDisplay("Take Profit Distance", "Optional take profit distance in price units", "Risk");
	}

	/// <summary>
	/// Candle type used for analysis.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Number of historical bars used in regression.
	/// </summary>
	public int BarsBack
	{
		get => _barsBack.Value;
		set => _barsBack.Value = value;
	}

	/// <summary>
	/// Polynomial regression degree.
	/// </summary>
	public int PolynomialDegree
	{
		get => _polynomialDegree.Value;
		set => _polynomialDegree.Value = value;
	}

	/// <summary>
	/// Standard deviation multiplier applied to channel width.
	/// </summary>
	public decimal StdMultiplier
	{
		get => _stdMultiplier.Value;
		set => _stdMultiplier.Value = value;
	}

	/// <summary>
	/// Optional stop loss distance expressed in price units.
	/// </summary>
	public decimal StopLossDistance
	{
		get => _stopLossDistance.Value;
		set => _stopLossDistance.Value = value;
	}

	/// <summary>
	/// Optional take profit distance expressed in price units.
	/// </summary>
	public decimal TakeProfitDistance
	{
		get => _takeProfitDistance.Value;
		set => _takeProfitDistance.Value = value;
	}

	/// <summary>
	/// Most recent lower band value.
	/// </summary>
	public decimal? CurrentLowerBand => _currentLowerBand;

	/// <summary>
	/// Most recent upper band value.
	/// </summary>
	public decimal? CurrentUpperBand => _currentUpperBand;

	/// <summary>
	/// Most recent regression center value.
	/// </summary>
	public decimal? CurrentCenter => _currentCenter;

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_closes.Clear();
		_entryPrice = null;
		_currentLowerBand = null;
		_currentUpperBand = null;
		_currentCenter = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		StartProtection(null, null);

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Store the latest close in the rolling window.
		UpdatePriceBuffer(candle.ClosePrice);

		if (_closes.Count < BarsBack + 1)
			return;

		// Skip trading when the regression cannot be calculated.
		if (!TryCalculateBands(out var center, out var upper, out var lower))
			return;

		_currentCenter = center;
		_currentUpperBand = upper;
		_currentLowerBand = lower;

		if (CheckLongExit(candle))
			return;

		// Exit long at upper band
		if (Position > 0 && candle.ClosePrice >= upper)
		{
			SellMarket();
			_entryPrice = null;
			return;
		}

		// Exit short at lower band
		if (Position < 0 && candle.ClosePrice <= lower)
		{
			BuyMarket();
			_entryPrice = null;
			return;
		}

		if (candle.ClosePrice <= lower && Position <= 0)
		{
			// Buy at lower band - mean reversion
			BuyMarket();
			_entryPrice = candle.ClosePrice;
		}
		else if (candle.ClosePrice >= upper && Position >= 0)
		{
			// Sell at upper band - mean reversion
			SellMarket();
			_entryPrice = candle.ClosePrice;
		}
	}

	private void UpdatePriceBuffer(decimal closePrice)
	{
		// Maintain a bounded queue with the most recent closes only.
		_closes.Enqueue(closePrice);

		var maxCount = BarsBack + 1;
		while (_closes.Count > maxCount)
		{
			_closes.Dequeue();
		}
	}

	private bool TryCalculateBands(out decimal center, out decimal upper, out decimal lower)
	{
		var degree = PolynomialDegree;
		var count = _closes.Count;
		var lookback = BarsBack;

		var closes = _closes.ToArray();
		var dataLength = lookback + 1;

		if (count < dataLength || degree < 1)
		{
			center = default;
			upper = default;
			lower = default;
			return false;
		}

		var size = degree + 1;
		var matrix = new double[size, size];
		var rhs = new double[size];
		var result = new double[size];
		var sumPowers = new double[2 * degree + 1];
		var data = new double[count];

		// Convert decimal closes to doubles for matrix calculations.
		for (var i = 0; i < count; i++)
		{
			data[i] = (double)closes[i];
		}

		// Pre-compute sums of powers for the normal equation matrix.
		for (var power = 0; power <= 2 * degree; power++)
		{
			double sum = 0;
			for (var n = 0; n <= lookback; n++)
			{
				sum += Math.Pow(n, power);
			}
			sumPowers[power] = sum;
		}

		for (var row = 0; row < size; row++)
		{
			for (var col = 0; col < size; col++)
			{
				matrix[row, col] = sumPowers[row + col];
			}

			double sum = 0;
			for (var n = 0; n <= lookback; n++)
			{
				var price = data[count - 1 - n];
				sum += price * Math.Pow(n, row);
			}
			rhs[row] = sum;
		}

		// Solve the linear system via Gaussian elimination to obtain the coefficients.
		if (!SolveLinearSystem(matrix, rhs, result))
		{
			center = default;
			upper = default;
			lower = default;
			return false;
		}

		var centerValue = result[0];
		if (double.IsNaN(centerValue) || double.IsInfinity(centerValue))
		{
			center = default;
			upper = default;
			lower = default;
			return false;
		}

		double total = 0;
		for (var i = count - dataLength; i < count; i++)
		{
			total += data[i];
		}
		var mean = total / dataLength;

		double variance = 0;
		for (var i = count - dataLength; i < count; i++)
		{
			var diff = data[i] - mean;
			variance += diff * diff;
		}
		variance /= dataLength;

		// Standard deviation of closes defines the envelope width.
		var std = Math.Sqrt(Math.Max(variance, 0)) * (double)StdMultiplier;
		if (double.IsNaN(std) || double.IsInfinity(std))
		{
			center = default;
			upper = default;
			lower = default;
			return false;
		}

		center = (decimal)centerValue;
		var stdDec = (decimal)std;

		upper = center + stdDec;
		lower = center - stdDec;
		return true;
	}

	private static bool SolveLinearSystem(double[,] matrix, double[] rhs, double[] result)
	{
		var size = rhs.Length;

		for (var k = 0; k < size; k++)
		{
			var pivotRow = k;
			var pivotValue = Math.Abs(matrix[k, k]);

			for (var i = k + 1; i < size; i++)
			{
				var value = Math.Abs(matrix[i, k]);
				if (value > pivotValue)
				{
					pivotValue = value;
					pivotRow = i;
				}
			}

			if (pivotValue < 1e-10)
				return false;

			if (pivotRow != k)
			{
				SwapRows(matrix, rhs, k, pivotRow);
			}

			var pivot = matrix[k, k];
			if (Math.Abs(pivot) < 1e-10)
				return false;

			for (var col = k; col < size; col++)
			{
				matrix[k, col] /= pivot;
			}
			rhs[k] /= pivot;

			for (var row = 0; row < size; row++)
			{
				if (row == k)
					continue;

				var factor = matrix[row, k];
				if (Math.Abs(factor) < 1e-12)
					continue;

				for (var col = k; col < size; col++)
				{
					matrix[row, col] -= factor * matrix[k, col];
				}
				rhs[row] -= factor * rhs[k];
			}
		}

		for (var i = 0; i < size; i++)
		{
			result[i] = rhs[i];
		}

		return true;
	}

	private static void SwapRows(double[,] matrix, double[] rhs, int rowA, int rowB)
	{
		var size = rhs.Length;

		for (var col = 0; col < size; col++)
		{
			(matrix[rowA, col], matrix[rowB, col]) = (matrix[rowB, col], matrix[rowA, col]);
		}

		(rhs[rowA], rhs[rowB]) = (rhs[rowB], rhs[rowA]);
	}

	private bool CheckLongExit(ICandleMessage candle)
	{
		// Evaluate optional protective exits using candle extremes.
		var exitPrice = _entryPrice;
		if (Position > 0 && exitPrice.HasValue)
		{
			var stopLoss = StopLossDistance;
			var takeProfit = TakeProfitDistance;
			var position = Position;

			if (stopLoss > 0m && candle.LowPrice <= exitPrice.Value - stopLoss)
			{
				SellMarket(position);
				_entryPrice = null;
				return true;
			}

			if (takeProfit > 0m && candle.HighPrice >= exitPrice.Value + takeProfit)
			{
				SellMarket(position);
				_entryPrice = null;
				return true;
			}
		}
		else if (Position <= 0)
		{
			_entryPrice = null;
		}

		return false;
	}
}