在 GitHub 上查看

AFStar 策略

AFStar 策略通过两步过滤寻找趋势反转:首先在一系列快/慢 EMA 组合中 扫描交叉信号,其次使用基于 Williams %R 的通道突破确认。只有当两种 条件同时满足时才会发出可执行的信号。

当某个快 EMA(范围为 [Start Fast, End Fast])从下方穿越某个慢 EMA (范围为 [Start Slow, End Slow],步长由 Step Period 控制)且 Williams %R 风险扫描指标在离开下轨之前处于中性区间时,会生成多头 箭头。空头箭头的判定完全对称。交易执行会按照 Signal Bar 参数 指定的已完成 K 线数量进行延迟,从而忠实复现原始 MQL5 专家的行为。

开仓后可选择性地附加以价格步长表示的止损和止盈。策略在每根收盘 K 线上检查这些保护水平。仓位规模由 Order Volume 控制,因此相较于 MQL5 版本采用了更简化的固定手数模型。

入场条件

  • 做多:
    • 至少一个快 EMA 向上穿越某个慢 EMA。
    • Williams %R 风险通道从下方突破。
    • 如果启用了 Enable Sell Exits,将在入场前平掉空头仓位。
  • 做空:
    • 对称条件(快 EMA 向下穿越慢 EMA,Williams %R 向下突破上轨)。
    • 若启用 Enable Buy Exits,将首先平掉多头仓位。

出场条件

  • 反向箭头在相应的退出开关允许时关闭仓位(买入箭头平空,卖出箭头平多)。
  • 可选的止损/止盈(以价格步长定义)可能导致提前离场。

参数

  • Order Volume – 市价单交易量。
  • Candle Type – 使用的 K 线周期(默认 4 小时)。
  • Start Fast / End Fast / Step Period – 快速 EMA 搜索范围。
  • Start Slow / End Slow – 慢速 EMA 搜索范围。
  • Start Risk / End Risk / Risk Step – Williams %R 风险扫描区间。
  • Signal Bar – 信号执行前需要等待的已完成 K 线数量。
  • Stop Loss (pips) – 止损距离(价格步长)。
  • Take Profit (pips) – 止盈距离(价格步长)。
  • Enable Buy Entries / Enable Sell Entries – 是否允许做多/做空入场。
  • Enable Buy Exits / Enable Sell Exits – 是否允许使用反向信号平仓。

说明

  • 策略最多保存 512 根最近的 K 线用于计算。
  • 若标的没有提供价格步长,则在计算止损/止盈时采用 1 作为步长。
  • 通过信号队列实现 Signal Bar = 0 时即时执行,较大的数值会按配置 延迟相应数量的完成 K 线。
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; }
	}
}