GitHub で見る

AFStar Strategy

The AFStar strategy scans for short-term momentum shifts by combining a wide range of fast/slow EMA crossovers with a Williams %R channel breakout filter. Only when both components agree does the strategy generate actionable signals.

A buy arrow is produced when at least one fast EMA (within the configured interval) crosses above a compatible slow EMA while the Williams %R based oscillator escapes from the lower band after staying inside the neutral zone. A sell arrow is generated by the symmetric conditions for bearish crossovers and an exit from the upper band. Signals are executed after the configured number of bars defined by the Signal Bar parameter, just like in the original MetaTrader expert.

Once a position is open the strategy can optionally attach protective stop loss and take profit levels expressed in price steps. Those protections are checked on every closed candle. All trades use the constant Order Volume parameter so the complex money-management rules from the MQL5 version are replaced with a simpler fixed-size approach.

Entry Logic

  • Long:
    • At least one fast EMA within [Start Fast, End Fast] rises above a slow EMA within [Start Slow, End Slow] using the Step Period increment.
    • The Williams %R channel, evaluated with risk values in the [Start Risk, End Risk] range and Risk Step, detects a break above the upper boundary after spending time inside the neutral band.
    • Optional short positions are closed beforehand when Enable Sell Exits is turned on.
  • Short:
    • Symmetric crossover and Williams %R breakout in the opposite direction.
    • Optional long exits occur first when Enable Buy Exits is enabled.

Exit Logic

  • Opposite arrows close positions when the corresponding exit flags are enabled (buy arrows close shorts, sell arrows close longs).
  • Optional stop loss and take profit levels measured in price steps can close positions earlier if price reaches those thresholds.

Parameters

  • Order Volume – trade size used for market orders.
  • Candle Type – timeframe for market data (defaults to 4-hour candles).
  • Start Fast / End Fast / Step Period – fast EMA range for crossover scan.
  • Start Slow / End Slow – slow EMA range paired with the fast EMA values.
  • Start Risk / End Risk / Risk Step – Williams %R risk scan boundaries.
  • Signal Bar – number of finished bars to wait before executing a signal.
  • Stop Loss (pips) – optional stop loss distance in price steps.
  • Take Profit (pips) – optional take profit distance in price steps.
  • Enable Buy Entries / Enable Sell Entries – allow long or short entries.
  • Enable Buy Exits / Enable Sell Exits – enable closing in the opposite direction.

Notes

  • The strategy keeps up to 512 recent candles to evaluate the AFStar logic.
  • If price steps are not available for the security, a value of 1 is used when calculating stop-loss and take-profit distances.
  • Signals are queued so that setting Signal Bar = 0 executes immediately, while higher values delay execution by that many completed bars.
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; }
	}
}