Auf GitHub ansehen

Surfen 3.0-Strategie

Überblick

Diese C#-Strategie ist eine originalgetreue Portierung des MetaTrader 4-Experten Surfing 3.0. Es stellt die Ausbruchslogik wieder her, die einen exponentiellen gleitenden Durchschnitt (EMA)-Umschlag beobachtet, der aus Kerzenhochs und -tiefs besteht. Immer wenn der vorherige Balken innerhalb des Bandes schließt und der zuletzt geschlossene Balken dieses durchbricht, reagiert das System mit einem Richtungshandel. Die Übersetzung basiert auf der hohen Ebene API von StockSharp, Kerzenabonnements und integrierten Indikatoren anstelle von handgeschriebenen Puffern.

Der Algorithmus arbeitet ausschließlich mit fertigen Kerzen aus einer konfigurierbaren Aggregation. Es behält nur die minimale Menge an Status bei, die zum Emulieren der vom Originalcode verwendeten iMA- und iClose-Lookbacks erforderlich ist. Jede Entscheidung wird einmal pro geschlossenem Balken getroffen, was dem Bewertungsstil „geschlossener Balken“ der MQL-Implementierung entspricht.

Indikatoren

  • Hoch EMA / Tief EMA – Zwei exponentielle gleitende Durchschnitte, berechnet auf Kerzenhochs und -tiefs. Sie bilden einen dynamischen Umschlag, der die Ausbruchsniveaus für Long- und Short-Einstiege definiert.
  • Relative Strength Index (RSI) – Fungiert als Trendfilter. Für Long-Positionen muss der RSI über LongRsiThreshold liegen, während Short-Positionen nur zulässig sind, wenn er unter ShortRsiThreshold liegt.

Handelslogik

  1. Abonnieren Sie Kerzen vom Typ CandleType und aktualisieren Sie die Indikatoren EMA und RSI für jeden fertigen Balken.
  2. Speichern Sie die vorherigen Schlussbalkenwerte des Schlusskurses und der EMA Hochs/Tiefs. Diese repräsentieren PriceClose_2, PriceHigh_2 und PriceLow_2 vom ursprünglichen Experten.
  3. Wenn der zuletzt geschlossene Balken (PriceClose_1) über das Hoch EMA kreuzt, während der vorherige Schlusskurs darunter oder gleich diesem Wert lag und der RSI-Filter Folgendes bestätigt:
    • Schließen Sie alle offenen Short-Positionen.
    • Eröffnen Sie eine Long-Market-Order mit einem Volumen von OrderVolume.
    • Berechnen Sie Stop-Loss und Take-Profit-Offsets in Instrumentenpunkten.
  4. Wenn der zuletzt geschlossene Balken unter den Tiefstwert EMA kreuzt, während der vorherige Schlusskurs darüber oder gleich diesem Wert lag und der RSI unter dem Short-Schwellenwert liegt:
    • Schließen Sie alle offenen Long-Positionen.
    • Eröffnen Sie eine Short-Market-Order mit einem Volumen von OrderVolume.
    • Tragen Sie die Schutzebenen in gleichen punktuellen Abständen auf.
  5. Es kann nur eine Nettoposition aktiv sein. Umkehrsignale glätten immer das bestehende Risiko, bevor sie in die entgegengesetzte Richtung eintreten.
  6. Außerhalb des Handelsfensters [TradeStartHour, TradeEndHour) werden keine neuen Geschäfte initiiert. Sobald die Uhr TradeEndHour erreicht, schließt die Strategie alle verbleibenden Positionen und setzt ihren internen Verlauf zurück, wobei sie den Aufruf closeAllPos() in der Version MQL nachahmt.

Risikomanagement

  • Stop-Loss / Take-Profit – Wird in Instrumentenpunkten ausgedrückt und anhand der Wertpapierpreisstufe umgerechnet. Beide sind optional; Wenn Sie einen Abstand von 0 festlegen, wird die entsprechende Ebene deaktiviert.
  • Session Flat – Am Ende des zulässigen Handelsfensters wird jede offene Position zum Marktwert geschlossen und die Stop/Take-Profit-Verfolgung wird gelöscht. Dies verhindert, dass Positionen über Nacht driften, genau wie es der ursprüngliche Experte mit startHour / endHour erzwungen hat.

Parameter

Name Beschreibung Standard
OrderVolume Handelsvolumen, das für jede Marktorder verwendet wird. 1
TakeProfitPoints Nehmen Sie die Gewinndistanz, ausgedrückt in Instrumentenpunkten. 80
StopLossPoints Stop-Loss-Distanz, ausgedrückt in Instrumentenpunkten. 50
MaPeriod Länge des EMA, angewendet auf Hochs und Tiefs. 50
RsiPeriod Zeitraum des Filters RSI. 10
LongRsiThreshold Mindestwert RSI erforderlich, um lange Einträge zuzulassen. 40
ShortRsiThreshold Maximal zulässiger RSI-Wert für die Eingabe von Short-Positionen. 65
TradeStartHour Stunde (Börsenzeit), ab der neue Geschäfte erlaubt sind. 8
TradeEndHour Stunde (exklusiv), nach der die Positionen geschlossen werden und keine neuen Trades beginnen. 18
CandleType Für alle Berechnungen verwendete Kerzenaggregation (Standard: 15-Minuten-Kerzen). 15m

Notizen

  • Signale werden ausschließlich an fertigen Kerzen ausgewertet; Intrabar-Schwankungen werden wie in MetaTrader ignoriert.
  • Die Strategie setzt ihren EMA-Verlauf zurück, wenn die Handelssitzung endet, um eine Vermischung von Daten verschiedener Tage zu vermeiden.
  • Auf eine Python-Übersetzung wird gemäß den Projektrichtlinien bewusst verzichtet.
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;
	}
}