Auf GitHub ansehen

Die durchschnittliche Kanalstrategie wurde korrigiert

Überblick

Die Corrected Average Channel Strategy ist eine C#-Portierung des MetaTrader-Expertenberaters e-CA-5. Das System erstellt den Indikator „Corrected Average“ (CA) jedes Mal neu, wenn eine Kerze schließt und eröffnet eine Position, wenn der Preis den korrigierten gleitenden Durchschnitt um einen konfigurierbaren Sigma-Offset kreuzt. Die konvertierte Implementierung basiert auf der High-Level-Kerze API von StockSharp, verwendet Marktaufträge und verwaltet intern schützende Exits (Stop-Loss, Take-Profit, Trailing Stop), um das Verhalten des ursprünglichen Expert Advisors widerzuspiegeln.

Korrigierter Durchschnittsindikator

Der CA-Filter kombiniert einen gleitenden Durchschnitt mit Volatilitätsrückmeldung. Die MQL-Version stellt drei Eingaben bereit: Länge des gleitenden Durchschnitts, Mittelungsmethode und angewandter Preis. Im StockSharp-Port:

  1. Der Typ des gleitenden Durchschnitts wird über den Parameter MaTypeOption (SMA, EMA, SMMA, LWMA) und die Länge MaPeriod ausgewählt.
  2. Ein StandardDeviation-Indikator mit demselben Zeitraum misst die aktuelle Volatilität.
  3. Für jede fertige Kerze wird der korrigierte Wert iterativ berechnet:
    • Sei M_t der MA-Wert des letzten Balkens und CA_{t-1} der korrigierte Wert des vorherigen Balkens.
    • Berechnen Sie v1 = StdDev_t^2 und v2 = (CA_{t-1} - M_t)^2.
    • Wenn v2 <= 0 oder v2 < v1, behalten Sie den Korrekturfaktor k = 0 bei. Andernfalls legen Sie k = 1 - v1 / v2 fest.
    • Aktualisieren Sie CA_t = CA_{t-1} + k * (M_t - CA_{t-1}).
    • Der allererste korrigierte Wert ist standardmäßig der gleitende Durchschnitt selbst.

Diese Rückkopplungsschleife dämpft den MA in ruhigen Zeiten und ermöglicht schnelle Anpassungen, wenn der Preis über die aktuelle Volatilitätsschätzung hinaus abweicht.

Handelslogik

  1. Die Strategie abonniert den konfigurierten Kerzentyp (CandleType) und wartet, bis sowohl der gleitende Durchschnitt als auch die Standardabweichung vollständig gebildet sind.
  2. Sobald eine Kerze endet, berechnet der Algorithmus den neuen korrigierten Wert und vergleicht den Schlusskurs der vorherigen Kerze mit dem vorherigen korrigierten Niveau.
  3. Zwei Sigma-Offsets, SigmaBuyPoints und SigmaSellPoints, werden mithilfe des PriceStep des Instruments in Preisabstände umgewandelt.
  4. Die Einstiegsregeln verwenden den vorherigen Kerzenschluss und das neu berechnete korrigierte Niveau:
    • Kaufen, wenn der vorherige Schlusskurs unter dem korrigierten Durchschnitt plus dem Kauf-Sigma lag und der aktuelle Schlusskurs über dieser Obergrenze endet.
    • Verkaufen, wenn der vorherige Schlusskurs über dem korrigierten Durchschnitt minus dem Verkaufs-Sigma lag und der aktuelle Schlusskurs unterhalb dieser unteren Grenze endet.
  5. Es ist nur eine Nettoposition zulässig. Ein neuer Trade wird nur eingereicht, wenn kein Exposure vorliegt.

Da die StockSharp-Version mit fertigen Kerzen arbeitet, erfolgt die Ausbruchsbestätigung einmal pro Balken statt bei jedem Tick, wodurch ein deterministisches Verhalten bereitgestellt wird, das für Backtesting und Live-Automatisierung mit Kerzendaten geeignet ist.

Risikomanagement

Der Port reproduziert alle drei Schutzmechanismen des ursprünglichen Expert Advisors:

  • Fester Stop-Loss: StopLossPoints multipliziert mit dem Preisschritt definiert den Abstand zwischen dem Einstiegspreis und dem schützenden Stop. Ein ausgelöster Stop schließt die gesamte Position mit einer Marktorder.
  • Fester Take-Profit: TakeProfitPoints wird in eine Gewinnzielentfernung umgewandelt. Wenn der Preis während einer Kerze das Niveau erreicht, wird die Position mit einer Marktorder geschlossen.
  • Trailing Stop: Wenn TrailingPoints größer als Null ist, verfolgt die Strategie nicht realisierte Gewinne und speichert, sobald der Preis mindestens um diese Distanz gestiegen ist, einen Trailing-Level hinter dem letzten Schlusskurs. Der Trailing Stop bewegt sich nur vorwärts und berücksichtigt TrailingStepPoints, was die minimale Verbesserung darstellt, bevor ein neues Trailing-Level akzeptiert wird. Nachfolgende Level werden mit Security.ShrinkPrice gerundet, sodass sie mit der Tick-Größe des Instruments übereinstimmen.

Alle Exits setzen den internen Risikostatus zurück. Wenn das nächste Signal erscheint, werden die Stop-, Ziel- und Trailing-Levels aus dem neuen Füllpreis neu berechnet, wodurch ein Verhalten sichergestellt wird, das der MQL-Version ähnelt, die den ursprünglichen Orderschutz ändert.

Parameter

Parameter Beschreibung
OrderVolume Menge, die für Markteintritte verwendet wird. Muss positiv sein.
TakeProfitPoints Gewinnziel in Preisschritten (0 deaktiviert den Take-Profit).
StopLossPoints Stop-Loss-Distanz in Preisschritten (0 deaktiviert den Stop-Loss).
TrailingPoints Erforderliche Gewinndistanz (in Preisschritten), bevor der Trailing Stop aktiviert wird.
TrailingStepPoints Minimale zusätzliche Distanz, die erfasst werden muss, bevor der Trailing Stop erneut verschoben wird.
MaPeriod Zeitraum sowohl des gleitenden Durchschnitts als auch der Standardabweichung.
MaTypeOption Typ des gleitenden Durchschnitts: SMA, EMA, SMMA oder LWMA.
SigmaBuyPoints Der Sigma-Offset wurde über dem korrigierten Durchschnitt hinzugefügt, bevor eine Long-Position eröffnet wurde.
SigmaSellPoints Der Sigma-Offset wurde unter den korrigierten Durchschnitt subtrahiert, bevor eine Short-Position eröffnet wurde.
CandleType Kerzenserien zur Indikatorberechnung und Signalauswertung.

Alle numerischen Parameter unterstützen die Optimierung durch SetCanOptimize(true), sodass die Strategie direkt in der StockSharp-Umgebung kalibriert werden kann.

Nutzungshinweise

  • Der Standardkerzentyp ist eine Stunde. Passen Sie es an den Zeitrahmen an, der bei der Optimierung der ursprünglichen MetaTrader-Strategie verwendet wurde.
  • Security.PriceStep wird verwendet, um alle „Punkte“-Eingaben in tatsächliche Preisentfernungen zu übersetzen. Instrumente ohne konfigurierten Schritt greifen auf 1 zurück, wodurch sinnvolles Verhalten für Indizes oder Kryptowährungen erhalten bleibt.
  • Die Strategie wird nur bei fertigen Kerzen ausgeführt. Wenn Intrabar-Präzision erforderlich ist, verringern Sie den Zeitrahmen auf die gewünschte Granularität.
  • Trailing-Stops werden bei Verletzung mit Marktaufträgen implementiert und ahmen den ursprünglichen EA nach, der die Stop-Loss-Preise modifizierte. Dieser Ansatz vermeidet die Platzierung zusätzlicher Stop-Orders und sorgt dafür, dass das Risikomanagement in der Strategie selbst enthalten bleibt.
  • Gemäß den Aufgabenanforderungen wird für diese Konvertierung keine Python-Version bereitgestellt.

Unterschiede zum Original EA

  • Das kerzenbasierte API von StockSharp ersetzt die Verarbeitung auf Tick-Ebene. Alle Entscheidungen werden getroffen, wenn eine Kerze schließt.
  • Die Auftragsverwaltung erfolgt saldiert: Gegensätzliche Positionen werden nicht gleichzeitig gehalten, was der Einzelauftragslogik der MetaTrader-Version entspricht.
  • Schutzstopps und Trailing-Exits werden über Marktaufträge ausgeführt, anstatt bestehende Auftragsscheine zu ändern. Dieses Verhalten ist bei Netting-Konten gleichwertig, wobei die Implementierung mit anderen StockSharp-Strategien konsistent bleibt.

Diese Anpassungen bewahren die Handelsidee von e-CA-5 und richten die Logik gleichzeitig an den Best Practices von StockSharp und den allgemeinen API-Konventionen aus, die in den Repository-Richtlinien beschrieben sind.

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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Port of the MetaTrader expert e-CA-5 that trades breakouts around the Corrected Average indicator.
/// The strategy subscribes to candles, rebuilds the indicator and places market orders when price crosses
/// the corrected moving average by the configured sigma offsets.
/// </summary>
public class CorrectedAverageChannelStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _trailingPoints;
	private readonly StrategyParam<int> _trailingStepPoints;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<MaTypes> _maType;
	private readonly StrategyParam<int> _sigmaBuyPoints;
	private readonly StrategyParam<int> _sigmaSellPoints;
	private readonly StrategyParam<DataType> _candleType;

	private DecimalLengthIndicator _ma;
	private StandardDeviation _std;

	private decimal _priceStep;
	private decimal _sigmaBuyOffset;
	private decimal _sigmaSellOffset;
	private decimal _stopLossDistance;
	private decimal _takeProfitDistance;
	private decimal _trailingDistance;
	private decimal _trailingStepDistance;

	private decimal? _previousCorrected;
	private decimal? _previousClose;

	private decimal? _entryPrice;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;
	private decimal _previousPosition;
	private decimal? _lastTradePrice;
	private Sides? _lastTradeSide;

	/// <summary>
	/// Order size used for market entries.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop trigger expressed in price steps.
	/// </summary>
	public int TrailingPoints
	{
		get => _trailingPoints.Value;
		set => _trailingPoints.Value = value;
	}

	/// <summary>
	/// Minimum increment required to advance the trailing stop in price steps.
	/// </summary>
	public int TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Moving average period used by the Corrected Average filter.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Moving average type replicated from the MetaTrader input.
	/// </summary>
	public MaTypes MaTypesOption
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	/// <summary>
	/// Buy-side sigma expressed in price steps.
	/// </summary>
	public int SigmaBuyPoints
	{
		get => _sigmaBuyPoints.Value;
		set => _sigmaBuyPoints.Value = value;
	}

	/// <summary>
	/// Sell-side sigma expressed in price steps.
	/// </summary>
	public int SigmaSellPoints
	{
		get => _sigmaSellPoints.Value;
		set => _sigmaSellPoints.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations and signal evaluation.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="CorrectedAverageChannelStrategy"/> class.
	/// </summary>
	public CorrectedAverageChannelStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Market order size used for entries", "Trading")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 60)
			.SetNotNegative()
			.SetDisplay("Take Profit (points)", "Distance from entry to the profit target in price steps", "Risk")
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 40)
			.SetNotNegative()
			.SetDisplay("Stop Loss (points)", "Distance from entry to the protective stop in price steps", "Risk")
			;

		_trailingPoints = Param(nameof(TrailingPoints), 0)
			.SetNotNegative()
			.SetDisplay("Trailing Trigger (points)", "Profit distance required before the trailing stop activates", "Risk")
			;

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 0)
			.SetNotNegative()
			.SetDisplay("Trailing Step (points)", "Minimum advance in price steps before the trailing stop moves", "Risk")
			;

		_maPeriod = Param(nameof(MaPeriod), 35)
			.SetRange(2, 500)
			.SetDisplay("MA Period", "Period of the moving average and standard deviation", "Indicator")
			;

		_maType = Param(nameof(MaTypesOption), MaTypes.Sma)
			.SetDisplay("MA Type", "Moving average type used inside the Corrected Average", "Indicator");

		_sigmaBuyPoints = Param(nameof(SigmaBuyPoints), 5)
			.SetNotNegative()
			.SetDisplay("Sigma BUY (points)", "Offset added above the corrected average before buying", "Signal")
			;

		_sigmaSellPoints = Param(nameof(SigmaSellPoints), 5)
			.SetNotNegative()
			.SetDisplay("Sigma SELL (points)", "Offset subtracted from the corrected average before selling", "Signal")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for calculations", "Data");
	}

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

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

		_ma = null;
		_std = null;
		_priceStep = 0m;
		_sigmaBuyOffset = 0m;
		_sigmaSellOffset = 0m;
		_stopLossDistance = 0m;
		_takeProfitDistance = 0m;
		_trailingDistance = 0m;
		_trailingStepDistance = 0m;
		_previousCorrected = null;
		_previousClose = null;
		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_previousPosition = 0m;
		_lastTradePrice = null;
		_lastTradeSide = null;
	}

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

		_ma = CreateMa(MaTypesOption, MaPeriod);
		_std = new StandardDeviation
		{
			Length = MaPeriod
		};

		_priceStep = Security?.PriceStep ?? 0m;
		if (_priceStep <= 0m)
		{
			_priceStep = 1m;
		}

		_sigmaBuyOffset = GetPriceOffset(SigmaBuyPoints);
		_sigmaSellOffset = GetPriceOffset(SigmaSellPoints);
		_stopLossDistance = GetPriceOffset(StopLossPoints);
		_takeProfitDistance = GetPriceOffset(TakeProfitPoints);
		_trailingDistance = GetPriceOffset(TrailingPoints);
		_trailingStepDistance = GetPriceOffset(TrailingStepPoints);

		Volume = OrderVolume;

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(_ma, _std, ProcessCandle).Start();
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade.Trade != null)
		{
			_lastTradePrice = trade.Trade.Price;
		}

		_lastTradeSide = trade.Order.Side;
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (_previousPosition == 0m && Position != 0m)
		{
			var entryPrice = _lastTradePrice ?? _previousClose;
			if (entryPrice is decimal price)
			{
				if (Position > 0m && _lastTradeSide == Sides.Buy)
				{
					InitializeRiskState(price, true);
				}
				else if (Position < 0m && _lastTradeSide == Sides.Sell)
				{
					InitializeRiskState(price, false);
				}
			}
		}
		else if (Position == 0m && _previousPosition != 0m)
		{
			ResetRiskState();
		}

		_previousPosition = Position;
	}

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

		if (_ma is null || _std is null)
			return;

		if (!_ma.IsFormed || !_std.IsFormed)
		{
			_previousCorrected = maValue;
			_previousClose = candle.ClosePrice;
			return;
		}

		var previousCorrected = _previousCorrected;
		var previousClose = _previousClose;

		decimal corrected;

		if (previousCorrected is not decimal prevCorrected)
		{
			corrected = maValue;
		}
		else
		{
			var diff = prevCorrected - maValue;
			var v2 = diff * diff;
			var v1 = stdValue * stdValue;
			var k = (v2 <= 0m || v2 < v1) ? 0m : 1m - (v1 / v2);
			corrected = prevCorrected + k * (maValue - prevCorrected);
		}

		if (HandleTrailing(candle))
		{
			_previousCorrected = corrected;
			_previousClose = candle.ClosePrice;
			return;
		}

		if (HandleRiskExit(candle))
		{
			_previousCorrected = corrected;
			_previousClose = candle.ClosePrice;
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_previousCorrected = corrected;
			_previousClose = candle.ClosePrice;
			return;
		}

		if (Position == 0m && previousCorrected is decimal prevCorr && previousClose is decimal prevCls)
		{
			var buyThreshold = corrected + _sigmaBuyOffset;
			var sellThreshold = corrected - _sigmaSellOffset;

			var buySignal = prevCls < prevCorr + _sigmaBuyOffset && candle.ClosePrice >= buyThreshold;
			var sellSignal = prevCls > prevCorr - _sigmaSellOffset && candle.ClosePrice <= sellThreshold;

			if (buySignal)
			{
				BuyMarket();
			}
			else if (sellSignal)
			{
				SellMarket();
			}
		}

		_previousCorrected = corrected;
		_previousClose = candle.ClosePrice;
	}

	private bool HandleTrailing(ICandleMessage candle)
	{
		if (_trailingDistance <= 0m || _entryPrice is null)
			return false;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
			return false;

		if (Position > 0m)
		{
			var moved = candle.ClosePrice - _entryPrice.Value;
			if (moved > _trailingDistance)
			{
				var candidate = candle.ClosePrice - _trailingDistance;
				if (_longTrailingStop is null || candidate - _longTrailingStop.Value >= _trailingStepDistance)
				{
					_longTrailingStop = Security?.ShrinkPrice(candidate) ?? candidate;
				}
			}

			if (_longTrailingStop is decimal trailing && candle.LowPrice <= trailing)
			{
				SellMarket(volume);
				ResetRiskState();
				return true;
			}
		}
		else if (Position < 0m)
		{
			var moved = _entryPrice.Value - candle.ClosePrice;
			if (moved > _trailingDistance)
			{
				var candidate = candle.ClosePrice + _trailingDistance;
				if (_shortTrailingStop is null || _shortTrailingStop.Value - candidate >= _trailingStepDistance)
				{
					_shortTrailingStop = Security?.ShrinkPrice(candidate) ?? candidate;
				}
			}

			if (_shortTrailingStop is decimal trailing && candle.HighPrice >= trailing)
			{
				BuyMarket(volume);
				ResetRiskState();
				return true;
			}
		}

		return false;
	}

	private bool HandleRiskExit(ICandleMessage candle)
	{
		var volume = Math.Abs(Position);
		if (volume <= 0m)
			return false;

		if (Position > 0m)
		{
			if (_stopLossPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(volume);
				ResetRiskState();
				return true;
			}

			if (_takeProfitPrice is decimal target && candle.HighPrice >= target)
			{
				SellMarket(volume);
				ResetRiskState();
				return true;
			}
		}
		else if (Position < 0m)
		{
			if (_stopLossPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(volume);
				ResetRiskState();
				return true;
			}

			if (_takeProfitPrice is decimal target && candle.LowPrice <= target)
			{
				BuyMarket(volume);
				ResetRiskState();
				return true;
			}
		}

		return false;
	}

	private void InitializeRiskState(decimal entryPrice, bool isLong)
	{
		_entryPrice = entryPrice;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;

		if (_stopLossDistance > 0m)
		{
			var rawPrice = isLong ? entryPrice - _stopLossDistance : entryPrice + _stopLossDistance;
			_stopLossPrice = Security?.ShrinkPrice(rawPrice) ?? rawPrice;
		}

		if (_takeProfitDistance > 0m)
		{
			var rawPrice = isLong ? entryPrice + _takeProfitDistance : entryPrice - _takeProfitDistance;
			_takeProfitPrice = Security?.ShrinkPrice(rawPrice) ?? rawPrice;
		}
	}

	private void ResetRiskState()
	{
		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
	}

	private decimal GetPriceOffset(int points)
	{
		if (points <= 0 || _priceStep <= 0m)
			return 0m;

		return points * _priceStep;
	}

	private static DecimalLengthIndicator CreateMa(MaTypes type, int length)
	{
		return type switch
		{
			MaTypes.Sma => new SMA { Length = length },
			MaTypes.Ema => new EMA { Length = length },
			MaTypes.Smma => new SmoothedMovingAverage { Length = length },
			MaTypes.Lwma => new WeightedMovingAverage { Length = length },
			_ => throw new ArgumentOutOfRangeException(nameof(type))
		};
	}

	/// <summary>
	/// Supported moving average types.
	/// </summary>
	public enum MaTypes
	{
		Sma,
		Ema,
		Smma,
		Lwma
	}
}