在 GitHub 上查看

Forex Profit 策略

来自 MetaTrader 的 “Forex Profit” 专家顾问的移植版本。策略在每根完成的K线上检查三条指数移动平均线是否排列,并用 Parabolic SAR 做趋势确认,然后按收盘价入场。风险控制包括不对称的止损/止盈距离、跟踪止损以及基于 EMA 反转的利润锁定。

细节

  • 入场条件
    • 多头:EMA10 同时高于 EMA25EMA50,上一根K线的 EMA10 不高于 EMA50,且 Parabolic SAR 位于上一收盘价之下。
    • 空头:EMA10 同时低于 EMA25EMA50,上一根K线的 EMA10 不低于 EMA50,且 Parabolic SAR 位于上一收盘价之上。
    • 每根完结K线只评估一次信号。
  • 出场条件
    • 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;
	}
}