Schwerpunkt-Mean-Reversion-Strategie
Die Strategie baut den vom ursprünglichen MQL4-Experten verwendeten Center of Gravity-Kanal neu auf, indem sie eine polynomiale Regression für die neuesten Kerzen löst. Das Regressionszentrum wird aus dem Schnittpunkt der Anpassung der kleinsten Quadrate berechnet, während die Bandbreite aus der Standardabweichung der Schlusskurse über denselben Lookback-Horizont abgeleitet wird. Das untere Band entspricht dem Regressionszentrum abzüglich der skalierten Abweichung und reproduziert den stdl-Puffer, auf den im Quellroboter zugegriffen wurde.
Während der Live-Verarbeitung unterhält der Algorithmus eine fortlaufende Warteschlange von Abschlüssen mit der durch den Parameter Bars Back definierten Länge. Jede fertige Kerze löst eine Neuberechnung der Regressionskoeffizienten durch Gaußsche Eliminierung auf dem normalen Gleichungssystem aus. Dadurch wird das Speichern vollständiger Kerzenverläufe vermieden, es ergibt sich jedoch die gleiche Kanalgeometrie wie beim benutzerdefinierten Indikator. Wenn die Matrix schlecht konditioniert wird, wird die Aktualisierung übersprungen, wodurch instabile Handelsentscheidungen verhindert werden.
Die Handelslogik spiegelt den ursprünglichen Experten wider: Wenn das aktuelle Kerzentief über dem unteren Abweichungsband (lowerBand < Low in der MQL-Notation) bleibt, betrachtet die Strategie dies als einen Absprung zur Mean-Reversion. Wenn keine Long-Position offen ist, wird ein etwaiges Short-Engagement geschlossen und eine Market-Buy-Order mit dem Strategievolumen erteilt. Die neuesten unteren, oberen und mittleren Werte werden über schreibgeschützte Eigenschaften für Diagramme oder Diagnosen angezeigt.
Das Risikomanagement ist optional. Stop-Loss-Distanz und Take-Profit-Distanz werden in absoluten Preiseinheiten angegeben. Wenn der Wert ungleich Null ist, zeichnet die Strategie den Einstiegspreis der aktiven Long-Position auf und prüft die Extremwerte der Kerze, um festzustellen, ob ein Stop- oder Gewinnziel erreicht wurde. Wenn keiner der Parameter angegeben ist, kann die Position manuell oder durch externe Module verwaltet werden.
Parameter
- Kerzentyp – Zeitrahmen des Kerzenabonnements, das die Regression speist.
- Bars Back – Anzahl der historischen Balken, die zur Berechnung des Regressionskanals verwendet werden (Standard 125).
- Polynomgrad – Grad der Polynomregression (Standard 2), die die Kanalkrümmung bestimmt.
- Std-Multiplikator – Multiplikator, der bei der Bildung des Umschlags auf die Standardabweichung angewendet wird (Standard 1).
- Stop-Loss-Distanz – optionaler Long-Stop-Loss-Offset in Preiseinheiten (Standard 0 deaktiviert ihn).
- Take-Profit-Abstand – optionaler Long-Take-Profit-Offset in Preiseinheiten (Standardeinstellung 0 deaktiviert ihn).
Die Strategie funktioniert nur bei abgeschlossenen Kerzen, verwendet Marktaufträge für Einstiege und führt keine automatischen Leerverkäufe durch, da der Verkaufszweig des ursprünglichen Experten auskommentiert wurde.
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()