Auf GitHub ansehen

Dynamic Averaging-Strategie

Überblick

Dynamic Averaging ist ein direkter Port des MetaTrader-5-Expert-Advisors „Dynamic averaging.mq5" (ID 23319). Die Strategie kombiniert einen schnellen Stochastic-Oszillator mit einem Volatilitätsfilter auf Basis der Standardabweichung. Trades sind nur erlaubt, solange die Marktvolatilität unter ihrem rollierenden Durchschnitt bleibt, was Einstiege in Konsolidierungsphasen erzwingt, in denen Stochastic-Umkehrungen zuverlässiger sind.

Parameter

  • TradeVolume – Ordergröße für jeden neuen Einstieg. Wird nach einer Verlustsequenz automatisch verdoppelt und nach einer Gewinnserie zurückgesetzt.
  • MinimumProfit – Schwebender Gewinn (in Kontowährung), der alle offenen Positionen schließt, sobald er überschritten wird.
  • SlidingWindowDays – Anzahl der Kalendertage für die Mittelung der Standardabweichungswerte und den Aufbau der Volatilitätsbasis.
  • StochasticKPeriod – Anzahl der Bars für die %K-Berechnung.
  • StochasticDPeriod – Glättungslänge für die %D-Linie.
  • StochasticSlowPeriod – Abschließende Verlangsamungsperiode des Stochastic-Oszillators.
  • StdDevPeriod – Rückblickperiode für den Standardabweichungsindikator.
  • CandleType – Quellkerzen für Berechnungen (Standard: 15-Minuten-Zeitrahmen).

Handelsregeln

  1. Die Strategie operiert ausschließlich auf abgeschlossenen Kerzen. Beim Schluss jedes Bars werden Stochastic- und Volatilitätsfilter über SubscribeCandles().BindEx aktualisiert.
  2. Marktvolatilität mit StandardDeviation(StdDevPeriod) berechnen und mit der durchschnittlichen Volatilität aus SimpleMovingAverage über die letzten SlidingWindowDays Bars vergleichen.
  3. Liegt die aktuelle Standardabweichung über dem rollierenden Durchschnitt, wird der Bar übersprungen.
  4. Bei gedämpfter Volatilität:
    • Long einsteigen, wenn %K unter 25 liegt und die Steigung der letzten zwei %K-Werte positiv ist (letzter Wert minus Wert vor zwei Bars).
    • Short einsteigen, wenn %K über 75 liegt und die Steigung der letzten zwei %K-Werte negativ ist.
  5. Positionen werden umgekehrt, indem genug Volumen gesendet wird, um die Gegenseite zu glätten und die neue TradeVolume-Exposition hinzuzufügen.
  6. Sobald der schwebende PnL der offenen Position MinimumProfit übersteigt, verlässt die Strategie den Markt sofort.

Positionsgrößen und Erholung

  • Die anfängliche Ordergröße entspricht TradeVolume.
  • Nach dem Schließen der Position wird die realisierte PnL-Änderung geprüft.
    • Ein Verlust verdoppelt die nächste Handelsgröße (Martingal-Schritt), um das ursprüngliche EA-Verhalten zu replizieren.
    • Gewinn oder Breakeven setzt die Größe auf das Basis-TradeVolume zurück.

Implementierungsdetails

  • Kerzen, Stochastic- und Standardabweichungswerte werden über die hochrangige API mit BindEx verarbeitet, was manuelle Pufferverwaltung vermeidet.
  • Das gleitende Volatilitätsfenster wandelt Kalendertage in Baranzahlen um, indem der Kerzen-Zeitrahmen verwendet wird, wenn verfügbar.
  • Die Schwebgewinn-Kontrolle basiert auf dem aktuellen Kerzenschluss und PositionAvgPrice, was der MQL-Implementierung entspricht, die nur den Gewinn offener Positionen summiert.
  • Alle Codekommentare sind auf Englisch; keine Python-Version gemäß Aufgabenanforderungen.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

public class DynamicAveragingStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public DynamicAveragingStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 50).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
		_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_fast = null; _slow = null;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };
		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
		if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}