该策略通过对最近的蜡烛进行多项式回归来重建原始 MQL4 专家使用的 Center of Gravity 通道。回归中心来自最小二乘解的常数项,通道宽度则来自同一观察窗口内的收盘价标准差。下轨等于中心减去放大后的标准差,对应于源码中调用的 stdl 缓冲区。
运行时维护一个长度为 Bars Back 的收盘价滚动队列。每当形成一根完整蜡烛时,策略都会使用高斯消元法求解正常方程组,重新计算回归系数。这样无需保存完整历史即可得到与自定义指标相同的通道结构。如果矩阵病态,计算会被跳过,以防止产生不稳定的交易决策。
交易逻辑继承自原始专家:当当前蜡烛的最低价仍高于下轨(MQL 表达为 lowerBand < Low)时,策略将其视为向均值回归的信号。如果没有持有多头头寸,则平掉所有空头并按策略的 Volume 参数买入。最新的中心值以及上下轨通过只读属性提供,方便绘图或诊断。
风控参数是可选的。Stop Loss Distance 和 Take 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class center_of_gravity_mean_reversion_strategy(Strategy):
"""
Center of Gravity regression channel mean reversion strategy.
Approximates price with polynomial regression and builds a standard deviation envelope.
Buys at lower band, sells at upper band.
"""
def __init__(self):
super(center_of_gravity_mean_reversion_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Timeframe used to build the regression channel", "General")
self._bars_back = self.Param("BarsBack", 125) \
.SetDisplay("Bars Back", "Number of historical bars used for regression", "Channel")
self._polynomial_degree = self.Param("PolynomialDegree", 2) \
.SetDisplay("Polynomial Degree", "Degree of polynomial regression", "Channel")
self._std_multiplier = self.Param("StdMultiplier", 1.0) \
.SetDisplay("Std Multiplier", "Multiplier applied to close price standard deviation", "Channel")
self._stop_loss_distance = self.Param("StopLossDistance", 0.0) \
.SetDisplay("Stop Loss Distance", "Optional stop loss distance in price units", "Risk")
self._take_profit_distance = self.Param("TakeProfitDistance", 0.0) \
.SetDisplay("Take Profit Distance", "Optional take profit distance in price units", "Risk")
self._closes = []
self._entry_price = 0.0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(center_of_gravity_mean_reversion_strategy, self).OnReseted()
self._closes = []
self._entry_price = 0.0
def OnStarted2(self, time):
super(center_of_gravity_mean_reversion_strategy, self).OnStarted2(time)
self._closes = []
self._entry_price = 0.0
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def on_process(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
bars_back = self._bars_back.Value
max_count = bars_back + 1
self._closes.append(close)
while len(self._closes) > max_count:
self._closes.pop(0)
if len(self._closes) < max_count:
return
result = self._try_calculate_bands()
if result is None:
return
center, upper, lower = result
if self._check_long_exit(candle):
return
if self.Position > 0 and close >= upper:
self.SellMarket()
self._entry_price = 0.0
return
if self.Position < 0 and close <= lower:
self.BuyMarket()
self._entry_price = 0.0
return
if close <= lower and self.Position <= 0:
self.BuyMarket()
self._entry_price = close
elif close >= upper and self.Position >= 0:
self.SellMarket()
self._entry_price = close
def _try_calculate_bands(self):
degree = self._polynomial_degree.Value
count = len(self._closes)
lookback = self._bars_back.Value
data_length = lookback + 1
if count < data_length or degree < 1:
return None
size = degree + 1
sum_powers = [0.0] * (2 * degree + 1)
for power in range(2 * degree + 1):
s = 0.0
for n in range(lookback + 1):
s += n ** power
sum_powers[power] = s
matrix = [[0.0] * size for _ in range(size)]
rhs = [0.0] * size
for row in range(size):
for col in range(size):
matrix[row][col] = sum_powers[row + col]
s = 0.0
for n in range(lookback + 1):
price = self._closes[count - 1 - n]
s += price * (n ** row)
rhs[row] = s
result = self._solve_linear_system(matrix, rhs, size)
if result is None:
return None
center_value = result[0]
if center_value != center_value: # NaN check
return None
total = 0.0
for i in range(count - data_length, count):
total += self._closes[i]
mean = total / data_length
variance = 0.0
for i in range(count - data_length, count):
diff = self._closes[i] - mean
variance += diff * diff
variance /= data_length
std = Math.Sqrt(max(variance, 0)) * self._std_multiplier.Value
center = center_value
upper = center + std
lower = center - std
return (center, upper, lower)
def _solve_linear_system(self, matrix, rhs, size):
for k in range(size):
pivot_row = k
pivot_value = abs(matrix[k][k])
for i in range(k + 1, size):
value = abs(matrix[i][k])
if value > pivot_value:
pivot_value = value
pivot_row = i
if pivot_value < 1e-10:
return None
if pivot_row != k:
matrix[k], matrix[pivot_row] = matrix[pivot_row], matrix[k]
rhs[k], rhs[pivot_row] = rhs[pivot_row], rhs[k]
pivot = matrix[k][k]
if abs(pivot) < 1e-10:
return None
for col in range(k, size):
matrix[k][col] /= pivot
rhs[k] /= pivot
for row in range(size):
if row == k:
continue
factor = matrix[row][k]
if abs(factor) < 1e-12:
continue
for col in range(k, size):
matrix[row][col] -= factor * matrix[k][col]
rhs[row] -= factor * rhs[k]
return rhs
def _check_long_exit(self, candle):
if self.Position > 0 and self._entry_price > 0:
stop_loss = self._stop_loss_distance.Value
take_profit = self._take_profit_distance.Value
if stop_loss > 0 and float(candle.LowPrice) <= self._entry_price - stop_loss:
self.SellMarket()
self._entry_price = 0.0
return True
if take_profit > 0 and float(candle.HighPrice) >= self._entry_price + take_profit:
self.SellMarket()
self._entry_price = 0.0
return True
elif self.Position <= 0:
self._entry_price = 0.0
return False
def CreateClone(self):
return center_of_gravity_mean_reversion_strategy()