Стратегия Center Of Gravity Mean Reversion
Стратегия воссоздаёт канал Center of Gravity из оригинального эксперта MQL4, решая задачу полиномиальной регрессии на последних барах. Центр канала берётся из свободного члена решения метода наименьших квадратов, а ширина формируется на основе стандартного отклонения цен закрытия за тот же интервал. Нижняя граница равна центру минус масштабированное отклонение, что соответствует буферу stdl, к которому обращался исходный робот.
Во время работы поддерживается скользящая очередь закрытий длиной Bars Back. Каждый завершённый бар инициирует пересчёт коэффициентов регрессии с помощью метода Гаусса для системы нормальных уравнений. Это позволяет обойтись без хранения полной истории свечей и даёт ту же геометрию канала, что и пользовательский индикатор. При вырожденной матрице пересчёт пропускается, чтобы избежать нестабильных торговых решений.
Торговая логика повторяет эксперта: когда минимум текущей свечи остаётся выше нижней границы отклонения (lowerBand < Low в обозначениях MQL), стратегия трактует это как отскок к среднему. При отсутствии длинной позиции закрываются возможные шорты и отправляется рыночная покупка с объёмом стратегии. Последние значения центра и границ доступны через свойства только для чтения и могут использоваться для визуализации.
Блок управления риском необязателен. 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()