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
- Abonnieren Sie Kerzen vom Typ
CandleType und aktualisieren Sie die Indikatoren EMA und RSI für jeden fertigen Balken.
- 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.
- 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.
- 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.
- Es kann nur eine Nettoposition aktiv sein. Umkehrsignale glätten immer das bestehende Risiko, bevor sie in die entgegengesetzte Richtung eintreten.
- 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import Math, TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage, RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
class surfing_30_strategy(Strategy):
def __init__(self):
super(surfing_30_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", 1.0)
self._tp_points = self.Param("TakeProfitPoints", 80)
self._sl_points = self.Param("StopLossPoints", 50)
self._ma_period = self.Param("MaPeriod", 50)
self._rsi_period = self.Param("RsiPeriod", 10)
self._long_rsi = self.Param("LongRsiThreshold", 30.0)
self._short_rsi = self.Param("ShortRsiThreshold", 70.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._prev_close = None
self._prev_sma = None
self._sl_price = None
self._tp_price = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def OrderVolume(self):
return self._order_volume.Value
@OrderVolume.setter
def OrderVolume(self, value):
self._order_volume.Value = value
def OnReseted(self):
super(surfing_30_strategy, self).OnReseted()
self._prev_close = None
self._prev_sma = None
self._sl_price = None
self._tp_price = None
def OnStarted2(self, time):
super(surfing_30_strategy, self).OnStarted2(time)
self.Volume = float(self.OrderVolume)
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
rsi = RelativeStrengthIndex()
rsi.Length = self._rsi_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(sma, rsi, self._process_candle).Start()
def _close_current_position(self):
if self.Position > 0:
self.SellMarket(float(self.Position))
elif self.Position < 0:
self.BuyMarket(abs(float(self.Position)))
def _set_targets(self, entry_price, is_long):
price_step = 1.0
if self.Security is not None and self.Security.PriceStep is not None and float(self.Security.PriceStep) > 0:
price_step = float(self.Security.PriceStep)
sl_pts = int(self._sl_points.Value)
tp_pts = int(self._tp_points.Value)
if is_long:
self._sl_price = entry_price - sl_pts * price_step if sl_pts > 0 else None
self._tp_price = entry_price + tp_pts * price_step if tp_pts > 0 else None
else:
self._sl_price = entry_price + sl_pts * price_step if sl_pts > 0 else None
self._tp_price = entry_price - tp_pts * price_step if tp_pts > 0 else None
def _reset_targets(self):
self._sl_price = None
self._tp_price = None
def _manage_active_position(self, candle):
if self.Position > 0:
if self._sl_price is not None and float(candle.LowPrice) <= self._sl_price:
self._close_current_position()
self._reset_targets()
return True
if self._tp_price is not None and float(candle.HighPrice) >= self._tp_price:
self._close_current_position()
self._reset_targets()
return True
elif self.Position < 0:
if self._sl_price is not None and float(candle.HighPrice) >= self._sl_price:
self._close_current_position()
self._reset_targets()
return True
if self._tp_price is not None and float(candle.LowPrice) <= self._tp_price:
self._close_current_position()
self._reset_targets()
return True
return False
def _process_candle(self, candle, sma_val, rsi_val):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
sma_v = float(sma_val)
rsi_v = float(rsi_val)
if self._manage_active_position(candle):
self._prev_close = close
self._prev_sma = sma_v
return
if self._prev_close is None or self._prev_sma is None:
self._prev_close = close
self._prev_sma = sma_v
return
prev_close = self._prev_close
prev_sma = self._prev_sma
buy_signal = prev_close <= prev_sma and close > sma_v and rsi_v > float(self._long_rsi.Value)
sell_signal = prev_close >= prev_sma and close < sma_v and rsi_v < float(self._short_rsi.Value)
if buy_signal and self.Position <= 0:
if self.Position < 0:
self._close_current_position()
self._reset_targets()
self.BuyMarket(float(self.OrderVolume))
self._set_targets(close, True)
elif sell_signal and self.Position >= 0:
if self.Position > 0:
self._close_current_position()
self._reset_targets()
self.SellMarket(float(self.OrderVolume))
self._set_targets(close, False)
self._prev_close = close
self._prev_sma = sma_v
def CreateClone(self):
return surfing_30_strategy()