Ver en GitHub

Estrategia de Surf 3.0

Descripción general

Esta estrategia de C# es una fiel adaptación del experto MetaTrader 4 Surfing 3.0. Recrea la lógica de ruptura que observa una envolvente de promedio móvil exponencial (EMA) construida a partir de máximos y mínimos de velas. Siempre que la barra anterior se cierra dentro de la banda y la última barra cerrada la atraviesa, el sistema reacciona con un comercio direccional. La traducción se basa en el alto nivel API de StockSharp, suscripciones de velas e indicadores integrados en lugar de buffers escritos a mano.

El algoritmo funciona exclusivamente con velas terminadas de una agregación configurable. Mantiene solo la cantidad mínima de estado necesaria para emular las retrospectivas iMA y iClose utilizadas por el código original. Cada decisión se toma una vez por barra cerrada, coincidiendo con el estilo de evaluación de "barra cerrada" de la implementación MQL.

Indicadores

  • Alto EMA / Mínimo EMA: dos promedios móviles exponenciales calculados sobre máximos y mínimos de velas. Forman una envolvente dinámica que define los niveles de ruptura para entradas largas y cortas.
  • Índice de fuerza relativa (RSI): actúa como filtro de tendencias. Las posiciones largas requieren que RSI esté por encima de LongRsiThreshold, mientras que las posiciones cortas solo se permiten cuando está por debajo de ShortRsiThreshold.

Lógica de trading

  1. Suscríbase a velas de tipo CandleType y actualice los indicadores EMA y RSI para cada barra terminada.
  2. Almacene los valores de la barra cerrada anterior del precio de cierre y los máximos/mínimos de EMA. Estos representan PriceClose_2, PriceHigh_2 y PriceLow_2 del experto original.
  3. Cuando la última barra cerrada (PriceClose_1) cruza por encima del máximo EMA mientras que el cierre anterior estaba por debajo o igual a él y el filtro RSI confirma:
    • Cierre cualquier posición corta abierta.
    • Abra una orden de mercado larga con volumen OrderVolume.
    • Calcule las compensaciones de stop loss y takeprofit en puntos del instrumento.
  4. Cuando la última barra cerrada cruza por debajo del mínimo EMA mientras que el cierre anterior estuvo por encima o igual a él y el RSI está por debajo del umbral corto:
    • Cierre cualquier posición larga abierta.
    • Abra una orden de mercado corta con volumen OrderVolume.
    • Aplique los niveles de protección utilizando las mismas distancias basadas en puntos.
  5. Sólo puede haber una posición neta activa. Las señales de reversión siempre aplanan la exposición existente antes de entrar en la dirección opuesta.
  6. Fuera de la ventana de negociación [TradeStartHour, TradeEndHour), no se inician nuevas transacciones. Una vez que el reloj llega a TradeEndHour, la estrategia cierra cualquier posición restante y restablece su historial interno, imitando la llamada closeAllPos() en la versión MQL.

Gestión del riesgo

  • Stop Loss / Take Profit: expresado en puntos del instrumento y convertido utilizando el paso del precio del valor. Ambos son opcionales; establecer una distancia de 0 desactiva el nivel respectivo.
  • Sesión plana: al final de la ventana de negociación permitida, todas las posiciones abiertas se cierran en el mercado y se borra el seguimiento de stop/takeprofit. Esto evita que las posiciones se desvíen de la noche a la mañana, exactamente como lo hizo cumplir el experto original con startHour / endHour.

Parámetros

Nombre Descripción Predeterminado
OrderVolume Volumen comercial utilizado para cada orden de mercado. 1
TakeProfitPoints Distancia de toma de ganancias expresada en puntos del instrumento. 80
StopLossPoints Distancia de stop loss expresada en puntos del instrumento. 50
MaPeriod Longitud del EMA aplicada a máximos y mínimos. 50
RsiPeriod Periodo del filtro RSI. 10
LongRsiThreshold Valor mínimo RSI requerido para permitir entradas largas. 40
ShortRsiThreshold Valor máximo RSI permitido para ingresar posiciones cortas. 65
TradeStartHour Hora (hora de cambio) a partir de la cual se permiten nuevas operaciones. 8
TradeEndHour Hora (exclusiva) después de la cual se cierran las posiciones y no se inician nuevas operaciones. 18
CandleType Agregación de velas utilizada para todos los cálculos (predeterminado: velas de 15 minutos). 15m

Notas

  • Las señales se evalúan estrictamente en velas terminadas; las fluctuaciones intrabar se ignoran como en MetaTrader.
  • La estrategia restablece su historial EMA cuando finaliza la sesión de negociación para evitar mezclar datos de diferentes días.
  • La traducción de Python se omite intencionalmente de acuerdo con las pautas del proyecto.
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;
	}
}