Auf GitHub ansehen

Surfing 3.0 Strategy

Overview

This C# strategy is a faithful port of the MetaTrader 4 expert Surfing 3.0. It recreates the breakout logic that watches an exponential moving average (EMA) envelope built from candle highs and lows. Whenever the previous bar closes inside the band and the latest closed bar pierces it, the system reacts with a directional trade. The translation relies on StockSharp's high level API, candle subscriptions and built-in indicators instead of hand-written buffers.

The algorithm works exclusively with finished candles from a configurable aggregation. It keeps only the minimal amount of state required to emulate the iMA and iClose lookbacks used by the original code. Every decision is made once per closed bar, matching the "closed bar" evaluation style of the MQL implementation.

Indicators

  • High EMA / Low EMA – Two exponential moving averages calculated on candle highs and lows. They form a dynamic envelope that defines breakout levels for long and short entries.
  • Relative Strength Index (RSI) – Acts as a trend filter. Long positions require the RSI to be above LongRsiThreshold, while shorts are allowed only when it is below ShortRsiThreshold.

Trading Logic

  1. Subscribe to candles of type CandleType and update the EMA and RSI indicators for every finished bar.
  2. Store the previous closed bar values of the close price and the EMA highs/lows. These represent PriceClose_2, PriceHigh_2 and PriceLow_2 from the original expert.
  3. When the latest closed bar (PriceClose_1) crosses above the high EMA while the previous close was below or equal to it and the RSI filter confirms:
    • Close any open short position.
    • Open a long market order with volume OrderVolume.
    • Calculate stop loss and take profit offsets in instrument points.
  4. When the latest closed bar crosses below the low EMA while the previous close was above or equal to it and the RSI is below the short threshold:
    • Close any open long position.
    • Open a short market order with volume OrderVolume.
    • Apply the protective levels using the same point-based distances.
  5. Only one net position can be active. Reversal signals always flatten the existing exposure before entering in the opposite direction.
  6. Outside the trading window [TradeStartHour, TradeEndHour), no new trades are initiated. Once the clock reaches TradeEndHour, the strategy closes any remaining position and resets its internal history, mimicking the closeAllPos() call in the MQL version.

Risk Management

  • Stop Loss / Take Profit – Expressed in instrument points and converted using the security price step. Both are optional; setting a distance of 0 disables the respective level.
  • Session Flat – At the end of the allowed trading window every open position is closed at market and the stop/take profit tracking is cleared. This prevents positions from drifting overnight, exactly as the original expert enforced with startHour / endHour.

Parameters

Name Description Default
OrderVolume Trade volume used for every market order. 1
TakeProfitPoints Take profit distance expressed in instrument points. 80
StopLossPoints Stop loss distance expressed in instrument points. 50
MaPeriod Length of the EMA applied to highs and lows. 50
RsiPeriod Period of the RSI filter. 10
LongRsiThreshold Minimum RSI value required to allow long entries. 40
ShortRsiThreshold Maximum RSI value allowed to enter short positions. 65
TradeStartHour Hour (exchange time) from which new trades are permitted. 8
TradeEndHour Hour (exclusive) after which positions are closed and no new trades start. 18
CandleType Candle aggregation used for all calculations (default: 15-minute candles). 15m

Notes

  • Signals are evaluated strictly on finished candles; intrabar fluctuations are ignored just like in MetaTrader.
  • The strategy resets its EMA history when the trading session ends to avoid mixing data from different days.
  • Python translation is intentionally omitted in accordance with the project guidelines.
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;
using StockSharp.Algo.Candles;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy that reproduces the Surfing 3.0 expert advisor logic from MetaTrader.
/// </summary>
public class Surfing30Strategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _longRsiThreshold;
	private readonly StrategyParam<decimal> _shortRsiThreshold;
	private readonly StrategyParam<int> _tradeStartHour;
	private readonly StrategyParam<int> _tradeEndHour;
	private readonly StrategyParam<DataType> _candleType;

	private RelativeStrengthIndex _rsi = null!;

	private decimal? _previousClose;
	private decimal? _previousHighEma;
	private decimal? _previousLowEma;

	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;

	/// <summary>
	/// Initialize <see cref="Surfing30Strategy"/>.
	/// </summary>
	public Surfing30Strategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume applied to every trade.", "Trading")
			
			.SetOptimize(0.1m, 5m, 0.1m);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 80)
			.SetNotNegative()
			.SetDisplay("Take Profit Points", "Distance to the take profit in instrument points.", "Risk Management")
			
			.SetOptimize(10, 200, 10);

		_stopLossPoints = Param(nameof(StopLossPoints), 50)
			.SetNotNegative()
			.SetDisplay("Stop Loss Points", "Distance to the stop loss in instrument points.", "Risk Management")
			
			.SetOptimize(10, 150, 10);

		_maPeriod = Param(nameof(MaPeriod), 50)
			.SetRange(1, 1000)
			.SetDisplay("EMA Period", "Length of the exponential moving averages calculated over highs and lows.", "Indicators")
			
			.SetOptimize(10, 120, 5);

		_rsiPeriod = Param(nameof(RsiPeriod), 10)
			.SetRange(1, 1000)
			.SetDisplay("RSI Period", "Length of the RSI filter.", "Indicators")
			
			.SetOptimize(5, 30, 1);

		_longRsiThreshold = Param(nameof(LongRsiThreshold), 30m)
			.SetDisplay("Long RSI Threshold", "Minimum RSI value required for long entries.", "Filters")
			
			.SetOptimize(20m, 60m, 5m);

		_shortRsiThreshold = Param(nameof(ShortRsiThreshold), 70m)
			.SetDisplay("Short RSI Threshold", "Maximum RSI value allowed for short entries.", "Filters")
			
			.SetOptimize(40m, 80m, 5m);

		_tradeStartHour = Param(nameof(TradeStartHour), 0)
			.SetDisplay("Trade Start Hour", "Hour of the day when new trades may start.", "Sessions")
			;

		_tradeEndHour = Param(nameof(TradeEndHour), 23)
			.SetDisplay("Trade End Hour", "Hour of the day when all positions are closed.", "Sessions")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Aggregation used for calculations.", "Data");
	}

	/// <summary>
	/// Volume used for every trade.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set
		{
			_orderVolume.Value = value;
			Volume = value;
		}
	}

	/// <summary>
	/// Distance to the take profit in instrument points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Distance to the stop loss in instrument points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Length of the exponential moving averages calculated over candle highs and lows.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Length of the RSI filter.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// Minimum RSI value required for long entries.
	/// </summary>
	public decimal LongRsiThreshold
	{
		get => _longRsiThreshold.Value;
		set => _longRsiThreshold.Value = value;
	}

	/// <summary>
	/// Maximum RSI value allowed for short entries.
	/// </summary>
	public decimal ShortRsiThreshold
	{
		get => _shortRsiThreshold.Value;
		set => _shortRsiThreshold.Value = value;
	}

	/// <summary>
	/// Hour of the day when new trades may start.
	/// </summary>
	public int TradeStartHour
	{
		get => _tradeStartHour.Value;
		set => _tradeStartHour.Value = value;
	}

	/// <summary>
	/// Hour of the day when all positions are closed.
	/// </summary>
	public int TradeEndHour
	{
		get => _tradeEndHour.Value;
		set => _tradeEndHour.Value = value;
	}

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

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

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

		_previousClose = null;
		_previousHighEma = null;
		_previousLowEma = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

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

		Volume = OrderVolume;

		var sma = new SimpleMovingAverage { Length = MaPeriod };
		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(sma, _rsi, ProcessCandle)
			.Start();
	}

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

		var currentClose = candle.ClosePrice;

		if (ManageActivePosition(candle))
		{
			UpdateHistory(currentClose, smaValue, smaValue);
			return;
		}

		if (_previousClose is null || _previousHighEma is null)
		{
			UpdateHistory(currentClose, smaValue, smaValue);
			return;
		}

		var previousClose = _previousClose.Value;
		var previousSma = _previousHighEma.Value;

		var buySignal = previousClose <= previousSma && currentClose > smaValue && rsiValue > LongRsiThreshold;
		var sellSignal = previousClose >= previousSma && currentClose < smaValue && rsiValue < ShortRsiThreshold;

		if (buySignal && Position <= 0)
		{
			if (Position < 0)
			{
				CloseCurrentPosition();
				ResetTargets();
			}

			BuyMarket(OrderVolume);
			SetTargets(currentClose, true);
		}
		else if (sellSignal && Position >= 0)
		{
			if (Position > 0)
			{
				CloseCurrentPosition();
				ResetTargets();
			}

			SellMarket(OrderVolume);
			SetTargets(currentClose, false);
		}

		UpdateHistory(currentClose, smaValue, smaValue);
	}

	private bool ManageActivePosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_stopLossPrice is not null && candle.LowPrice <= _stopLossPrice)
			{
				CloseCurrentPosition();
				ResetTargets();
				return true;
			}

			if (_takeProfitPrice is not null && candle.HighPrice >= _takeProfitPrice)
			{
				CloseCurrentPosition();
				ResetTargets();
				return true;
			}
		}
		else if (Position < 0)
		{
			if (_stopLossPrice is not null && candle.HighPrice >= _stopLossPrice)
			{
				CloseCurrentPosition();
				ResetTargets();
				return true;
			}

			if (_takeProfitPrice is not null && candle.LowPrice <= _takeProfitPrice)
			{
				CloseCurrentPosition();
				ResetTargets();
				return true;
			}
		}

		return false;
	}

	private void CloseCurrentPosition()
	{
		if (Position > 0m)
			SellMarket(Position);
		else if (Position < 0m)
			BuyMarket(Math.Abs(Position));
	}

	private void SetTargets(decimal entryPrice, bool isLong)
	{
		var priceStep = Security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
			priceStep = 1m;

		if (isLong)
		{
			_stopLossPrice = StopLossPoints > 0 ? entryPrice - StopLossPoints * priceStep : null;
			_takeProfitPrice = TakeProfitPoints > 0 ? entryPrice + TakeProfitPoints * priceStep : null;
		}
		else
		{
			_stopLossPrice = StopLossPoints > 0 ? entryPrice + StopLossPoints * priceStep : null;
			_takeProfitPrice = TakeProfitPoints > 0 ? entryPrice - TakeProfitPoints * priceStep : null;
		}
	}

	private void ResetTargets()
	{
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

	private void UpdateHistory(decimal currentClose, decimal currentHighEma, decimal currentLowEma)
	{
		_previousClose = currentClose;
		_previousHighEma = currentHighEma;
		_previousLowEma = currentLowEma;
	}

	private void ResetHistory()
	{
		_previousClose = null;
		_previousHighEma = null;
		_previousLowEma = null;
	}

	private bool IsWithinTradeHours(DateTimeOffset time)
	{
		var startHour = TradeStartHour;
		var endHour = TradeEndHour;

		if (endHour <= startHour)
			return time.Hour >= startHour || time.Hour < endHour;

		return time.Hour >= startHour && time.Hour < endHour;
	}
}