La estrategia reconstruye el canal del centro de gravedad utilizado por el experto original MQL4 resolviendo una regresión polinómica en las velas más recientes. El centro de regresión se calcula a partir de la intersección del ajuste de mínimos cuadrados, mientras que el ancho de la banda se deriva de la desviación estándar de los precios de cierre en el mismo horizonte retrospectivo. La banda inferior es igual al centro de regresión menos la desviación escalada, lo que reproduce el búfer stdl al que se accede en el robot de origen.
Durante el procesamiento en vivo, el algoritmo mantiene una cola continua de cierres con la longitud definida por el parámetro Bars Back. Cada vela terminada desencadena un nuevo cálculo de los coeficientes de regresión mediante eliminación gaussiana en el sistema de ecuaciones normal. Esto evita almacenar historiales completos de velas pero produce la misma geometría de canal que el indicador personalizado. Si la matriz queda mal acondicionada, se omite la actualización, lo que evita decisiones comerciales inestables.
La lógica comercial refleja la del experto original: cuando el mínimo de la vela actual se mantiene por encima de la banda de desviación inferior (lowerBand < Low en notación MQL), la estrategia lo considera un rebote de reversión a la media. Si no hay ninguna posición larga abierta, cualquier exposición corta se cierra y se emite una orden de compra de mercado utilizando el volumen de la estrategia. Los valores inferior, superior y central más recientes se exponen mediante propiedades de solo lectura para gráficos o diagnósticos.
La gestión de riesgos es opcional. Distancia Stop Loss y Distancia Take Profit se especifican en unidades de precio absoluto. Cuando es distinto de cero, la estrategia registra el precio de entrada de la posición larga activa y verifica los extremos de las velas para determinar si se ha alcanzado un objetivo de parada o de ganancias. Si no se proporciona ninguno de los parámetros, la posición se puede gestionar manualmente o mediante módulos externos.
Parámetros
- Tipo de vela: período de tiempo de la suscripción de la vela que alimenta la regresión.
- Bars Back: número de barras históricas utilizadas para calcular el canal de regresión (predeterminado 125).
- Grado polinómico: grado de la regresión polinómica (predeterminado 2) que rige la curvatura del canal.
- Std Multiplier: multiplicador aplicado a la desviación estándar al formar la envolvente (predeterminado 1).
- Distancia de Stop Loss: compensación opcional de stop loss largo en unidades de precio (el valor predeterminado 0 lo desactiva).
- Distancia de obtención de beneficios: compensación opcional de obtención de beneficios a largo plazo en unidades de precio (el valor predeterminado 0 lo desactiva).
La estrategia opera únicamente con velas completadas, utiliza órdenes de mercado para las entradas y no realiza ventas en corto automáticas porque la rama de venta del experto original fue comentada.
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()