Auf GitHub ansehen

AFStar-Strategie

Die AFStar-Strategie sucht nach kurzfristigen Impulsveränderungen, indem sie eine breite Palette von schnellen/langsamen EMA-Kreuzungen mit einem Williams-%R-Kanalausbruchsfilter kombiniert. Nur wenn beide Komponenten übereinstimmen, erzeugt die Strategie umsetzbare Signale.

Ein Kaufpfeil wird erzeugt, wenn mindestens eine schnelle EMA (innerhalb des konfigurierten Intervalls) eine kompatible langsame EMA von unten kreuzt, während der auf Williams %R basierende Oszillator das untere Band verlässt, nachdem er sich innerhalb der neutralen Zone aufgehalten hat. Ein Verkaufspfeil wird durch die symmetrischen Bedingungen für bärische Kreuzungen und einen Ausbruch aus dem oberen Band erzeugt. Signale werden nach der konfigurierten Anzahl von Bars, definiert durch den Parameter Signal Bar, ausgeführt – genau wie beim ursprünglichen MetaTrader-Experten.

Sobald eine Position eröffnet ist, kann die Strategie optional schützende Stop-Loss- und Take-Profit-Levels in Preisschritten anhängen. Diese Schutzmaßnahmen werden bei jeder geschlossenen Kerze überprüft. Alle Trades verwenden den konstanten Parameter Order Volume, sodass die komplexen Geldverwaltungsregeln der MQL5-Version durch einen einfacheren Festbetragsansatz ersetzt werden.

Einstiegslogik

  • Long:
    • Mindestens eine schnelle EMA innerhalb von [Start Fast, End Fast] steigt über eine langsame EMA innerhalb von [Start Slow, End Slow] mit dem Inkrement Step Period.
    • Der Williams-%R-Kanal, bewertet mit Risikowerten im Bereich [Start Risk, End Risk] und Risk Step, erkennt einen Ausbruch über die obere Grenze nach einer Verweildauer innerhalb des neutralen Bandes.
    • Optionale Short-Positionen werden vorher geschlossen, wenn Enable Sell Exits aktiviert ist.
  • Short:
    • Symmetrischer Kreuzungsausbruch und Williams-%R-Ausbruch in entgegengesetzter Richtung.
    • Optionale Long-Ausstiege erfolgen zuerst, wenn Enable Buy Exits aktiviert ist.

Ausstiegslogik

  • Entgegengesetzte Pfeile schließen Positionen, wenn die entsprechenden Ausstiegs-Flags aktiviert sind (Kaufpfeile schließen Shorts, Verkaufspfeile schließen Longs).
  • Optionale Stop-Loss- und Take-Profit-Levels in Preisschritten können Positionen früher schließen, wenn der Preis diese Schwellen erreicht.

Parameter

  • Order Volume – Handelsgröße für Marktorders.
  • Candle Type – Zeitrahmen für Marktdaten (Standard: 4-Stunden-Kerzen).
  • Start Fast / End Fast / Step Period – schnelle EMA-Spanne für den Kreuzungsscan.
  • Start Slow / End Slow – langsame EMA-Spanne, die mit den schnellen EMA-Werten gepaart wird.
  • Start Risk / End Risk / Risk Step – Williams-%R-Risikoscan-Grenzen.
  • Signal Bar – Anzahl abgeschlossener Bars, die vor der Ausführung eines Signals gewartet wird.
  • Stop Loss (pips) – optionaler Stop-Loss-Abstand in Preisschritten.
  • Take Profit (pips) – optionaler Take-Profit-Abstand in Preisschritten.
  • Enable Buy Entries / Enable Sell Entries – Long- oder Short-Einstiege erlauben.
  • Enable Buy Exits / Enable Sell Exits – Schließen in entgegengesetzter Richtung aktivieren.

Hinweise

  • Die Strategie hält bis zu 512 aktuelle Kerzen vor, um die AFStar-Logik auszuwerten.
  • Falls Preisschritte für das Wertpapier nicht verfügbar sind, wird bei der Berechnung von Stop-Loss- und Take-Profit-Abständen der Wert 1 verwendet.
  • Signale werden in eine Warteschlange eingereiht, sodass Signal Bar = 0 sofort ausführt, während höhere Werte die Ausführung um diese Anzahl abgeschlossener Bars verzögern.
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>
/// AFStar strategy converted from MetaTrader 5 expert advisor.
/// It searches for fast and slow EMA crossovers across a configurable range
/// and confirms them with a dynamic Williams %R channel breakout.
/// </summary>
public class AfStarStrategy : Strategy
{
	private readonly StrategyParam<int> _rangeLength;
	private readonly StrategyParam<int> _maxHistory;

	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _startFast;
	private readonly StrategyParam<decimal> _endFast;
	private readonly StrategyParam<decimal> _startSlow;
	private readonly StrategyParam<decimal> _endSlow;
	private readonly StrategyParam<decimal> _stepPeriod;
	private readonly StrategyParam<decimal> _startRisk;
	private readonly StrategyParam<decimal> _endRisk;
	private readonly StrategyParam<decimal> _stepRisk;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<bool> _enableBuyEntries;
	private readonly StrategyParam<bool> _enableSellEntries;
	private readonly StrategyParam<bool> _enableBuyExits;
	private readonly StrategyParam<bool> _enableSellExits;

	private readonly List<ICandleMessage> _candles = new();
	private readonly List<decimal> _value2History = new();
	private readonly List<AfStarSignal> _signalQueue = new();
	private decimal _lastWpr;

	private bool _prevBuy1;
	private bool _prevSell1;
	private bool _prevBuy2;
	private bool _prevSell2;

	private decimal? _longStopLevel;
	private decimal? _longTakeLevel;
	private decimal? _shortStopLevel;
	private decimal? _shortTakeLevel;

	/// <summary>
	/// Initializes a new instance of <see cref="AfStarStrategy"/>.
	/// </summary>
	public AfStarStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Volume used for market orders", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
		.SetDisplay("Candle Type", "Time frame for candles", "General");

		_startFast = Param(nameof(StartFast), 3m)
		.SetGreaterThanZero()
		.SetDisplay("Start Fast", "Lower bound for fast EMA period", "Indicator");

		_endFast = Param(nameof(EndFast), 3.5m)
		.SetGreaterThanZero()
		.SetDisplay("End Fast", "Upper bound for fast EMA period", "Indicator");

		_startSlow = Param(nameof(StartSlow), 8m)
		.SetGreaterThanZero()
		.SetDisplay("Start Slow", "Lower bound for slow EMA period", "Indicator");

		_endSlow = Param(nameof(EndSlow), 9m)
		.SetGreaterThanZero()
		.SetDisplay("End Slow", "Upper bound for slow EMA period", "Indicator");

		_stepPeriod = Param(nameof(StepPeriod), 0.2m)
		.SetGreaterThanZero()
		.SetDisplay("Period Step", "Increment for scanning EMA periods", "Indicator");

		_startRisk = Param(nameof(StartRisk), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Start Risk", "Lower bound for risk scan", "Williams %R");

		_endRisk = Param(nameof(EndRisk), 2.8m)
		.SetGreaterThanZero()
		.SetDisplay("End Risk", "Upper bound for risk scan", "Williams %R");

		_stepRisk = Param(nameof(StepRisk), 0.5m)
		.SetGreaterThanZero()
		.SetDisplay("Risk Step", "Increment for risk parameter", "Williams %R");

		_rangeLength = Param(nameof(RangeLength), 10)
		.SetRange(1, 200)
		.SetDisplay("Range Length", "Bars used to compute the average range filter", "Indicator");

		_maxHistory = Param(nameof(MaxHistory), 512)
		.SetRange(10, 5000)
		.SetDisplay("Max History", "Maximum candles stored for calculations", "General");

		_signalBar = Param(nameof(SignalBar), 1)
		.SetRange(0, 10)
		.SetDisplay("Signal Bar", "Delay in bars before executing a signal", "Trading");

		_stopLossPips = Param(nameof(StopLossPips), 1000)
		.SetRange(0, 100000)
		.SetDisplay("Stop Loss (pips)", "Stop loss distance in price steps", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 2000)
		.SetRange(0, 100000)
		.SetDisplay("Take Profit (pips)", "Take profit distance in price steps", "Risk");

		_enableBuyEntries = Param(nameof(BuyEntriesEnabled), true)
		.SetDisplay("Enable Buy Entries", "Allow long entries on buy signals", "Trading");

		_enableSellEntries = Param(nameof(SellEntriesEnabled), true)
		.SetDisplay("Enable Sell Entries", "Allow short entries on sell signals", "Trading");

		_enableBuyExits = Param(nameof(BuyExitsEnabled), true)
		.SetDisplay("Enable Buy Exits", "Allow closing longs on sell signals", "Trading");

		_enableSellExits = Param(nameof(SellExitsEnabled), true)
		.SetDisplay("Enable Sell Exits", "Allow closing shorts on buy signals", "Trading");
	}

	/// <summary>
	/// Trade volume used for market orders.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Lower bound for the fast EMA search.
	/// </summary>
	public decimal StartFast
	{
		get => _startFast.Value;
		set => _startFast.Value = value;
	}

	/// <summary>
	/// Upper bound for the fast EMA search.
	/// </summary>
	public decimal EndFast
	{
		get => _endFast.Value;
		set => _endFast.Value = value;
	}

	/// <summary>
	/// Lower bound for the slow EMA search.
	/// </summary>
	public decimal StartSlow
	{
		get => _startSlow.Value;
		set => _startSlow.Value = value;
	}

	/// <summary>
	/// Upper bound for the slow EMA search.
	/// </summary>
	public decimal EndSlow
	{
		get => _endSlow.Value;
		set => _endSlow.Value = value;
	}

	/// <summary>
	/// Step used when scanning EMA periods.
	/// </summary>
	public decimal StepPeriod
	{
		get => _stepPeriod.Value;
		set => _stepPeriod.Value = value;
	}

	/// <summary>
	/// Lower bound for the risk parameter scan.
	/// </summary>
	public decimal StartRisk
	{
		get => _startRisk.Value;
		set => _startRisk.Value = value;
	}

	/// <summary>
	/// Upper bound for the risk parameter scan.
	/// </summary>
	public decimal EndRisk
	{
		get => _endRisk.Value;
		set => _endRisk.Value = value;
	}

	/// <summary>
	/// Step for the risk parameter scan.
	/// </summary>
	public decimal StepRisk
	{
		get => _stepRisk.Value;
		set => _stepRisk.Value = value;
	}

	/// <summary>
	/// Number of bars used to calculate the average range filter.
	/// </summary>
	public int RangeLength
	{
		get => _rangeLength.Value;
		set => _rangeLength.Value = value;
	}

	/// <summary>
	/// Maximum number of stored candles for calculations.
	/// </summary>
	public int MaxHistory
	{
		get => _maxHistory.Value;
		set
		{
			_maxHistory.Value = value;
			TrimHistory();
		}
	}

	/// <summary>
	/// Bars to wait before executing a signal.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

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

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

	/// <summary>
	/// Allow long entries.
	/// </summary>
	public bool BuyEntriesEnabled
	{
		get => _enableBuyEntries.Value;
		set => _enableBuyEntries.Value = value;
	}

	/// <summary>
	/// Allow short entries.
	/// </summary>
	public bool SellEntriesEnabled
	{
		get => _enableSellEntries.Value;
		set => _enableSellEntries.Value = value;
	}

	/// <summary>
	/// Allow closing long positions on sell signals.
	/// </summary>
	public bool BuyExitsEnabled
	{
		get => _enableBuyExits.Value;
		set => _enableBuyExits.Value = value;
	}

	/// <summary>
	/// Allow closing short positions on buy signals.
	/// </summary>
	public bool SellExitsEnabled
	{
		get => _enableSellExits.Value;
		set => _enableSellExits.Value = value;
	}

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

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

		_candles.Clear();
		_value2History.Clear();
		_signalQueue.Clear();
		_lastWpr = -50m;
		_prevBuy1 = false;
		_prevSell1 = false;
		_prevBuy2 = false;
		_prevSell2 = false;
		_longStopLevel = null;
		_longTakeLevel = null;
		_shortStopLevel = null;
		_shortTakeLevel = null;
	}

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

		Volume = OrderVolume;

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(ProcessCandle)
		.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

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

		// manual indicators, no bound check needed

		_candles.Add(candle);
		_value2History.Add(0m);

		if (_candles.Count > MaxHistory)
		{
			_candles.RemoveAt(0);
			if (_value2History.Count > 0)
			_value2History.RemoveAt(0);
		}

		var signal = ComputeSignal();

		ApplyStops(candle);

		if (signal.HasValue)
		{
			_signalQueue.Add(signal.Value);

			while (_signalQueue.Count > SignalBar)
			{
				var activeSignal = _signalQueue[0];
				try { _signalQueue.RemoveAt(0); } catch { break; }
				ExecuteSignal(activeSignal, candle);
			}
		}
	}

	private void TrimHistory()
	{
		while (_candles.Count > MaxHistory)
		{
			_candles.RemoveAt(0);
		}

		while (_value2History.Count > MaxHistory)
		{
			_value2History.RemoveAt(0);
		}
	}

	private AfStarSignal? ComputeSignal()
	{
		if (_candles.Count < GetMinHistory())
		return null;

		var buy1 = false;
		var sell1 = false;

		foreach (var slow in EnumerateRange(StartSlow, EndSlow, StepPeriod))
		{
			foreach (var fast in EnumerateRange(StartFast, EndFast, StepPeriod))
			{
				var slowPer = 2m / (slow + 1m);
				var fastPer = 2m / (fast + 1m);

				var slowCurrent = GetClose(0) * slowPer + GetClose(1) * (1m - slowPer);
				var slowPrevious = GetClose(1) * slowPer + GetClose(2) * (1m - slowPer);
				var fastCurrent = GetClose(0) * fastPer + GetClose(1) * (1m - fastPer);
				var fastPrevious = GetClose(1) * fastPer + GetClose(2) * (1m - fastPer);

				if (!buy1 && fastPrevious < slowPrevious && fastCurrent > slowCurrent)
				{
					buy1 = true;
					break;
				}

				if (!sell1 && fastPrevious > slowPrevious && fastCurrent < slowCurrent)
				{
					sell1 = true;
					break;
				}
			}

			if (buy1 || sell1)
			break;
		}

		var range = ComputeAverageRange();
		var mro1 = FindMro1(range);
		var mro2 = FindMro2(range);
		var value2 = 0m;
		var hasBuy2 = false;
		var hasSell2 = false;

		foreach (var risk in EnumerateRange(StartRisk, EndRisk, StepRisk))
		{
			var value10 = 3m + risk * 2m;
			var x1 = 67m + risk;
			var x2 = 33m - risk;

			var value11 = value10;
			value11 = mro1 > -1 ? 3m : value10;
			value11 = mro2 > -1 ? 4m : value10;

			var period = Math.Max(1, (int)value11);
			var wpr = GetWilliamsR(period);
			value2 = 100m - Math.Abs(wpr);

			if (!hasSell2 && value2 < x2)
			{
				var offset = 1;
				while (TryGetPrevValue2(offset, out var prev) && prev >= x2 && prev <= x1)
				offset++;

				if (TryGetPrevValue2(offset, out var prevOutside) && prevOutside > x1)
				hasSell2 = true;
			}

			if (!hasBuy2 && value2 > x1)
			{
				var offset = 1;
				while (TryGetPrevValue2(offset, out var prev) && prev >= x2 && prev <= x1)
				offset++;

				if (TryGetPrevValue2(offset, out var prevOutside) && prevOutside < x2)
				hasBuy2 = true;
			}

			if (hasBuy2 || hasSell2)
			break;
		}

		var buySignal = (buy1 && hasBuy2) || (buy1 && _prevBuy2) || (_prevBuy1 && hasBuy2);
		var sellSignal = (sell1 && hasSell2) || (sell1 && _prevSell2) || (_prevSell1 && hasSell2);

		if (buySignal && sellSignal)
		{
			buySignal = false;
			sellSignal = false;
		}

		_prevBuy1 = buy1;
		_prevSell1 = sell1;
		_prevBuy2 = hasBuy2;
		_prevSell2 = hasSell2;

		_value2History[^1] = value2;

		return new AfStarSignal(buySignal, sellSignal);
	}

	private void ExecuteSignal(AfStarSignal signal, ICandleMessage candle)
	{
		if (signal.BuyArrow)
		{
			if (SellExitsEnabled)
			ExitShort();

			if (BuyEntriesEnabled && Position == 0)
			{
				BuyMarket();
				InitializeLongTargets(candle.ClosePrice);
			}
		}

		if (signal.SellArrow)
		{
			if (BuyExitsEnabled)
			ExitLong();

			if (SellEntriesEnabled && Position == 0)
			{
				SellMarket();
				InitializeShortTargets(candle.ClosePrice);
			}
		}
	}

	private void ApplyStops(ICandleMessage candle)
	{
		var position = Position;

		if (position > 0)
		{
			if (_longStopLevel.HasValue && candle.LowPrice <= _longStopLevel.Value)
			{
				ExitLong();
				position = Position;
			}
			else if (_longTakeLevel.HasValue && candle.HighPrice >= _longTakeLevel.Value)
			{
				ExitLong();
				position = Position;
			}
		}

		if (position < 0)
		{
			if (_shortStopLevel.HasValue && candle.HighPrice >= _shortStopLevel.Value)
			{
				ExitShort();
			}
			else if (_shortTakeLevel.HasValue && candle.LowPrice <= _shortTakeLevel.Value)
			{
				ExitShort();
			}
		}
	}

	private void ExitLong()
	{
		var position = Position;
		if (position > 0)
		{
			SellMarket();
			ResetLongTargets();
		}
	}

	private void ExitShort()
	{
		var position = Position;
		if (position < 0)
		{
			BuyMarket();
			ResetShortTargets();
		}
	}

	private void InitializeLongTargets(decimal entryPrice)
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
		step = 1m;

		_longStopLevel = StopLossPips > 0 ? entryPrice - step * StopLossPips : null;
		_longTakeLevel = TakeProfitPips > 0 ? entryPrice + step * TakeProfitPips : null;
	}

	private void InitializeShortTargets(decimal entryPrice)
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
		step = 1m;

		_shortStopLevel = StopLossPips > 0 ? entryPrice + step * StopLossPips : null;
		_shortTakeLevel = TakeProfitPips > 0 ? entryPrice - step * TakeProfitPips : null;
	}

	private void ResetLongTargets()
	{
		_longStopLevel = null;
		_longTakeLevel = null;
	}

	private void ResetShortTargets()
	{
		_shortStopLevel = null;
		_shortTakeLevel = null;
	}

	private int GetMinHistory()
	{
		return 12 + 3 + SignalBar;
	}

	private decimal ComputeAverageRange()
	{
		var sum = 0m;
		for (var i = 0; i < RangeLength; i++)
		{
			sum += Math.Abs(GetHigh(i) - GetLow(i));
		}

		return sum / RangeLength;
	}

	private int FindMro1(decimal range)
	{
		for (var offset = 0; offset < 9; offset++)
		{
			if (Math.Abs(GetOpen(offset) - GetClose(offset + 1)) >= range * 2m)
			return offset;
		}

		return -1;
	}

	private int FindMro2(decimal range)
	{
		for (var offset = 0; offset < 6; offset++)
		{
			if (Math.Abs(GetClose(offset + 3) - GetClose(offset)) >= range * 4.6m)
			return offset;
		}

		return -1;
	}

	private decimal GetClose(int offset)
	{
		return _candles[^(offset + 1)].ClosePrice;
	}

	private decimal GetOpen(int offset)
	{
		return _candles[^(offset + 1)].OpenPrice;
	}

	private decimal GetHigh(int offset)
	{
		return _candles[^(offset + 1)].HighPrice;
	}

	private decimal GetLow(int offset)
	{
		return _candles[^(offset + 1)].LowPrice;
	}

	private decimal GetWilliamsR(int period)
	{
		var maxHigh = GetHigh(0);
		var minLow = GetLow(0);

		for (var i = 1; i < period && i < _candles.Count; i++)
		{
			var high = GetHigh(i);
			var low = GetLow(i);

			if (high > maxHigh)
			maxHigh = high;

			if (low < minLow)
			minLow = low;
		}

		var close = GetClose(0);
		var range = maxHigh - minLow;

		if (range == 0m)
		{
			return _lastWpr;
		}

		var wpr = -(maxHigh - close) * 100m / range;
		_lastWpr = wpr;
		return wpr;
	}

	private bool TryGetPrevValue2(int offset, out decimal value)
	{
		var index = _value2History.Count - 1 - offset;
		if (index >= 0)
		{
			value = _value2History[index];
			return true;
		}

		value = 0m;
		return false;
	}

	private IEnumerable<decimal> EnumerateRange(decimal start, decimal end, decimal step)
	{
		if (step <= 0m)
		yield break;

		if (start <= end)
		{
			for (var value = start; value <= end + 0.0000001m; value += step)
			yield return value;
		}
		else
		{
			for (var value = start; value >= end - 0.0000001m; value -= step)
			yield return value;
		}
	}

	private readonly struct AfStarSignal
	{
		public AfStarSignal(bool buyArrow, bool sellArrow)
		{
			BuyArrow = buyArrow;
			SellArrow = sellArrow;
		}

		public bool BuyArrow { get; }
		public bool SellArrow { get; }
	}
}