Открыть на GitHub

Стратегия Forex Profit

Перевод советника MetaTrader «Forex Profit». Стратегия ожидает совпадения трёх экспоненциальных средних и подтверждения от Parabolic SAR, после чего входит в рынок по цене закрытия сформированной свечи. Управление рисками включает несимметричные уровни стоп-лосса и тейк-профита, трейлинг-стоп и дополнительную фиксацию прибыли по развороту EMA.

Детали

  • Условия входа:
    • Лонг: EMA10 выше EMA25 и EMA50, значение EMA10 на предыдущей свече не выше EMA50, Parabolic SAR ниже прошлого закрытия.
    • Шорт: EMA10 ниже EMA25 и EMA50, значение EMA10 на предыдущей свече не ниже EMA50, Parabolic SAR выше прошлого закрытия.
    • Сигналы проверяются только один раз для каждой завершённой свечи.
  • Условия выхода:
    • Закрыть лонг, когда EMA10 опускается ниже своего прошлого значения и текущая прибыль превышает ProfitThreshold.
    • Закрыть шорт, когда EMA10 поднимается выше прошлого значения и текущая прибыль превышает ProfitThreshold.
    • Стоп-лосс и тейк-профит задаются при открытии сделки (разные дистанции для лонга и шорта).
    • Трейлинг-стоп активируется после движения цены на TrailingStopPoints от входа и сдвигается шагами TrailingStepPoints.
  • Стопы: Да — фиксированный стоп-лосс, фиксированный тейк-профит и трейлинг.
  • Значения по умолчанию:
    • 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 час
  • Дополнительные замечания:
    • Расстояния стопов и целей задаются в шагах цены инструмента и автоматически переводятся с учётом тика.
    • Закрытия по прибыли рассчитываются от общего результата позиции (с учётом объёма) в валюте счёта через преобразование тиковой стоимости.
    • Трейлинг поддерживает стоп вслед за ценой без перескоков меньше указанного шага.
  • Фильтры:
    • Категория: Trend following
    • Направление: Лонг и Шорт
    • Индикаторы: EMA, Parabolic SAR
    • Стопы: Да (фиксированные + трейлинг)
    • Сложность: Средняя
    • Таймфрейм: Настраиваемый (по умолчанию 1 час)
    • Сезонность: Нет
    • Нейросети: Нет
    • Дивергенция: Нет
    • Риск: Средний
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;
	}
}