Auf GitHub ansehen

Forex Profit Strategy

Translation of the "Forex Profit" MetaTrader expert advisor. The strategy waits for alignment of three exponential moving averages and confirmation from Parabolic SAR before entering trades on the close of each finished candle. Risk is controlled through asymmetric stop-loss and take-profit distances, a trailing stop and an additional EMA-based profit lock.

Details

  • Entry Criteria:
    • Long: EMA10 above both EMA25 and EMA50, previous bar's EMA10 at or below EMA50, and Parabolic SAR below the prior close.
    • Short: EMA10 below both EMA25 and EMA50, previous bar's EMA10 at or above EMA50, and Parabolic SAR above the prior close.
    • Signals are evaluated only once per completed candle.
  • Exit Criteria:
    • Close long when EMA10 turns below its previous value and current profit exceeds the ProfitThreshold.
    • Close short when EMA10 turns above its previous value and current profit exceeds the ProfitThreshold.
    • Protective stop-loss and take-profit levels set at order entry (different distances for longs vs shorts).
    • Trailing stop activates after price moves TrailingStopPoints beyond the entry and is updated by TrailingStepPoints increments.
  • Stops: Yes — fixed stop-loss, fixed take-profit, and trailing stop management.
  • Default Values:
    • FastEmaLength = 10
    • MediumEmaLength = 25
    • SlowEmaLength = 50
    • TakeProfitBuyPoints = 55
    • TakeProfitSellPoints = 65
    • StopLossBuyPoints = 60
    • StopLossSellPoints = 85
    • TrailingStopPoints = 74
    • TrailingStepPoints = 5
    • ProfitThreshold = 10
    • SarAcceleration = 0.02
    • SarMaxAcceleration = 0.2
    • Volume = 1
    • CandleType = 1 hour timeframe
  • Additional Notes:
    • Stop/target distances are expressed in instrument price steps and converted automatically using the security's tick size.
    • Profit-based exits rely on the total position profit (including volume) converted from price ticks to account currency.
    • Trailing logic keeps the stop behind price swings without overshooting the configured step.
  • Filters:
    • Category: Trend following
    • Direction: Long & Short
    • Indicators: EMA, Parabolic SAR
    • Stops: Yes (fixed + trailing)
    • Complexity: Intermediate
    • Timeframe: Configurable (default 1 hour)
    • Seasonality: No
    • Neural networks: No
    • Divergence: No
    • Risk level: Medium
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>
/// Trend-following strategy translated from the "Forex Profit" MQL expert.
/// Combines EMA alignment with Parabolic SAR confirmation and dynamic exits.
/// </summary>
public class ForexProfitStrategy : Strategy
{
	private readonly StrategyParam<int> _fastEmaLength;
	private readonly StrategyParam<int> _mediumEmaLength;
	private readonly StrategyParam<int> _slowEmaLength;
	private readonly StrategyParam<decimal> _takeProfitBuyPoints;
	private readonly StrategyParam<decimal> _takeProfitSellPoints;
	private readonly StrategyParam<decimal> _stopLossBuyPoints;
	private readonly StrategyParam<decimal> _stopLossSellPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<decimal> _profitThreshold;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _sarAcceleration;
	private readonly StrategyParam<decimal> _sarMaxAcceleration;

	private ExponentialMovingAverage _emaFast;
	private ExponentialMovingAverage _emaMedium;
	private ExponentialMovingAverage _emaSlow;
	private ParabolicSar _sar;

	private decimal? _ema10Prev;
	private decimal? _ema10PrevPrev;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;

	/// <summary>
	/// Fast EMA length.
	/// </summary>
	public int FastEmaLength
	{
		get => _fastEmaLength.Value;
		set => _fastEmaLength.Value = value;
	}

	/// <summary>
	/// Medium EMA length.
	/// </summary>
	public int MediumEmaLength
	{
		get => _mediumEmaLength.Value;
		set => _mediumEmaLength.Value = value;
	}

	/// <summary>
	/// Slow EMA length.
	/// </summary>
	public int SlowEmaLength
	{
		get => _slowEmaLength.Value;
		set => _slowEmaLength.Value = value;
	}

	/// <summary>
	/// Take profit distance for long positions in price steps.
	/// </summary>
	public decimal TakeProfitBuyPoints
	{
		get => _takeProfitBuyPoints.Value;
		set => _takeProfitBuyPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance for short positions in price steps.
	/// </summary>
	public decimal TakeProfitSellPoints
	{
		get => _takeProfitSellPoints.Value;
		set => _takeProfitSellPoints.Value = value;
	}

	/// <summary>
	/// Stop loss distance for long positions in price steps.
	/// </summary>
	public decimal StopLossBuyPoints
	{
		get => _stopLossBuyPoints.Value;
		set => _stopLossBuyPoints.Value = value;
	}

	/// <summary>
	/// Stop loss distance for short positions in price steps.
	/// </summary>
	public decimal StopLossSellPoints
	{
		get => _stopLossSellPoints.Value;
		set => _stopLossSellPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in price steps.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Minimum step for trailing stop updates in price steps.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Minimal profit in account currency required to exit on EMA reversal.
	/// </summary>
	public decimal ProfitThreshold
	{
		get => _profitThreshold.Value;
		set => _profitThreshold.Value = value;
	}


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

	/// <summary>
	/// Initial Parabolic SAR acceleration.
	/// </summary>
	public decimal SarAcceleration
	{
		get => _sarAcceleration.Value;
		set => _sarAcceleration.Value = value;
	}

	/// <summary>
	/// Maximum Parabolic SAR acceleration.
	/// </summary>
	public decimal SarMaxAcceleration
	{
		get => _sarMaxAcceleration.Value;
		set => _sarMaxAcceleration.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="ForexProfitStrategy"/>.
	/// </summary>
	public ForexProfitStrategy()
	{
		_fastEmaLength = Param(nameof(FastEmaLength), 10)
		.SetGreaterThanZero()
		.SetDisplay("Fast EMA Length", "Length of the fast EMA", "Averages")
		;

		_mediumEmaLength = Param(nameof(MediumEmaLength), 25)
		.SetGreaterThanZero()
		.SetDisplay("Medium EMA Length", "Length of the medium EMA", "Averages")
		;

		_slowEmaLength = Param(nameof(SlowEmaLength), 50)
		.SetGreaterThanZero()
		.SetDisplay("Slow EMA Length", "Length of the slow EMA", "Averages")
		;

		_takeProfitBuyPoints = Param(nameof(TakeProfitBuyPoints), 55m)
		.SetGreaterThanZero()
		.SetDisplay("Take Profit Long", "Take profit distance for buys (points)", "Risk")
		;

		_takeProfitSellPoints = Param(nameof(TakeProfitSellPoints), 65m)
		.SetGreaterThanZero()
		.SetDisplay("Take Profit Short", "Take profit distance for sells (points)", "Risk")
		;

		_stopLossBuyPoints = Param(nameof(StopLossBuyPoints), 60m)
		.SetGreaterThanZero()
		.SetDisplay("Stop Loss Long", "Stop loss distance for buys (points)", "Risk")
		;

		_stopLossSellPoints = Param(nameof(StopLossSellPoints), 85m)
		.SetGreaterThanZero()
		.SetDisplay("Stop Loss Short", "Stop loss distance for sells (points)", "Risk")
		;

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 74m)
		.SetNotNegative()
		.SetDisplay("Trailing Stop", "Trailing stop distance (points)", "Risk")
		;

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m)
		.SetNotNegative()
		.SetDisplay("Trailing Step", "Minimal trailing step (points)", "Risk")
		;

		_profitThreshold = Param(nameof(ProfitThreshold), 10m)
		.SetNotNegative()
		.SetDisplay("Profit Threshold", "Profit required for EMA exit", "Risk")
		;


		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe for calculations", "General");

		_sarAcceleration = Param(nameof(SarAcceleration), 0.02m)
		.SetGreaterThanZero()
		.SetDisplay("SAR Start", "Initial SAR acceleration", "Indicators")
		;

		_sarMaxAcceleration = Param(nameof(SarMaxAcceleration), 0.2m)
		.SetGreaterThanZero()
		.SetDisplay("SAR Max", "Maximum SAR acceleration", "Indicators")
		;
	}

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

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

		_ema10Prev = null;
		_ema10PrevPrev = null;
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
	}

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

		_emaFast = new EMA { Length = FastEmaLength };
		_emaMedium = new EMA { Length = MediumEmaLength };
		_emaSlow = new EMA { Length = SlowEmaLength };
		_sar = new ParabolicSar
		{
			Acceleration = SarAcceleration,
			AccelerationMax = SarMaxAcceleration
		};

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _emaFast);
			DrawIndicator(area, _emaMedium);
			DrawIndicator(area, _emaSlow);
			DrawIndicator(area, _sar);
			DrawOwnTrades(area);
		}
	}

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

		var ema10Prev = _ema10Prev;
		var ema10PrevPrev = _ema10PrevPrev;

		var median = (candle.HighPrice + candle.LowPrice) / 2m;
		var isFinal = candle.State == CandleStates.Finished;
		var ema10Value = _emaFast.Process(new DecimalIndicatorValue(_emaFast, median, candle.OpenTime) { IsFinal = isFinal }).ToDecimal();
		var ema25Value = _emaMedium.Process(new DecimalIndicatorValue(_emaMedium, median, candle.OpenTime) { IsFinal = isFinal }).ToDecimal();
		var ema50Value = _emaSlow.Process(new DecimalIndicatorValue(_emaSlow, median, candle.OpenTime) { IsFinal = isFinal }).ToDecimal();
		var sarResult = _sar.Process(candle);

		if (!_emaSlow.IsFormed || !_sar.IsFormed)
		{
			_ema10PrevPrev = ema10Prev;
			_ema10Prev = ema10Value;
			return;
		}

		var sarValue = sarResult.ToDecimal();

		var step = Security?.PriceStep ?? 1m;
		if (step <= 0m)
			step = 1m;

		var stepPrice = step;

		var longSignal = ema10Value > ema25Value &&
			ema10Value > ema50Value &&
			ema10PrevPrev.HasValue &&
			ema10PrevPrev.Value <= ema50Value &&
			sarValue < candle.ClosePrice;

		var shortSignal = ema10Value < ema25Value &&
			ema10Value < ema50Value &&
			ema10PrevPrev.HasValue &&
			ema10PrevPrev.Value >= ema50Value &&
			sarValue > candle.ClosePrice;

		if (Position == 0m && IsFormedAndOnlineAndAllowTrading())
		{
			if (longSignal)
			{
				TryEnterLong(candle, step);
			}
			else if (shortSignal)
			{
				TryEnterShort(candle, step);
			}
		}
		else if (Position > 0m)
		{
			ManageLongPosition(candle, ema10Value, ema10Prev, step, stepPrice);
		}
		else if (Position < 0m)
		{
			ManageShortPosition(candle, ema10Value, ema10Prev, step, stepPrice);
		}

		_ema10PrevPrev = ema10Prev;
		_ema10Prev = ema10Value;
	}

	private void TryEnterLong(ICandleMessage candle, decimal step)
	{
		if (Volume <= 0m)
			return;

		BuyMarket(Volume);

		var entry = candle.ClosePrice;
		_entryPrice = entry;
		_stopPrice = entry - step * StopLossBuyPoints;
		_takeProfitPrice = entry + step * TakeProfitBuyPoints;
	}

	private void TryEnterShort(ICandleMessage candle, decimal step)
	{
		if (Volume <= 0m)
			return;

		SellMarket(Volume);

		var entry = candle.ClosePrice;
		_entryPrice = entry;
		_stopPrice = entry + step * StopLossSellPoints;
		_takeProfitPrice = entry - step * TakeProfitSellPoints;
	}

	private void ManageLongPosition(ICandleMessage candle, decimal ema10Value, decimal? ema10Prev, decimal step, decimal stepPrice)
	{
		if (_entryPrice == null)
			return;

		var profit = ComputeProfit(candle.ClosePrice, step, stepPrice);

		if (ema10Prev.HasValue && ema10Value < ema10Prev.Value && profit > ProfitThreshold)
		{
				SellMarket(Math.Abs(Position));
			ResetPositionTargets();
			return;
		}

		if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
		{
				SellMarket(Math.Abs(Position));
			ResetPositionTargets();
			return;
		}

		if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
		{
				SellMarket(Math.Abs(Position));
			ResetPositionTargets();
			return;
		}

		UpdateLongTrailing(candle, step);
	}

	private void ManageShortPosition(ICandleMessage candle, decimal ema10Value, decimal? ema10Prev, decimal step, decimal stepPrice)
	{
		if (_entryPrice == null)
			return;

		var profit = ComputeProfit(candle.ClosePrice, step, stepPrice);

		if (ema10Prev.HasValue && ema10Value > ema10Prev.Value && profit > ProfitThreshold)
		{
				BuyMarket(Math.Abs(Position));
			ResetPositionTargets();
			return;
		}

		if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
		{
				BuyMarket(Math.Abs(Position));
			ResetPositionTargets();
			return;
		}

		if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
		{
				BuyMarket(Math.Abs(Position));
			ResetPositionTargets();
			return;
		}

		UpdateShortTrailing(candle, step);
	}

	private void UpdateLongTrailing(ICandleMessage candle, decimal step)
	{
		if (TrailingStopPoints <= 0m || _entryPrice == null)
			return;

		var trailingDistance = step * TrailingStopPoints;
		var trailingStep = step * TrailingStepPoints;
		var movement = candle.ClosePrice - _entryPrice.Value;

		if (movement > trailingDistance)
		{
			var newStop = candle.ClosePrice - trailingDistance;

			if (!_stopPrice.HasValue || newStop - _stopPrice.Value >= trailingStep)
				_stopPrice = newStop;
		}
	}

	private void UpdateShortTrailing(ICandleMessage candle, decimal step)
	{
		if (TrailingStopPoints <= 0m || _entryPrice == null)
			return;

		var trailingDistance = step * TrailingStopPoints;
		var trailingStep = step * TrailingStepPoints;
		var movement = _entryPrice.Value - candle.ClosePrice;

		if (movement > trailingDistance)
		{
			var newStop = candle.ClosePrice + trailingDistance;

			if (!_stopPrice.HasValue || _stopPrice.Value - newStop >= trailingStep)
				_stopPrice = newStop;
		}
	}

	private decimal ComputeProfit(decimal currentPrice, decimal step, decimal stepPrice)
	{
		if (_entryPrice == null || Position == 0m)
			return 0m;

		var ticks = (currentPrice - _entryPrice.Value) / step;
		return ticks * stepPrice * Position;
	}

	private void ResetPositionTargets()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
	}
}