Ver en GitHub

Estrategia Ema612CrossoverStrategy

Resumen

  • Port del asesor experto de MetaTrader 5 "EMA 6.12 (edición de barabashkakvn)" a la API de alto nivel de StockSharp.
  • Negocia el cruce entre una media móvil simple rápida y una lenta (el script original también usaba MODE_SMA a pesar de su nombre EMA).
  • Añade gestión opcional de take profit y trailing stop expresados en unidades de precio absolutas para que el comportamiento pueda ajustarse por instrumento.

Lógica de trading

Preparación de datos

  • La estrategia se suscribe a velas del tipo definido por CandleType (marco temporal de 15 minutos por defecto).
  • Se calculan dos medias móviles simples: longitud FastPeriod para la curva rápida y longitud SlowPeriod para la curva lenta. El período lento debe ser mayor que el período rápido.

Reglas de entrada

  • Las señales se evalúan al cierre de cada vela terminada.
  • Un cruce alcista ocurre cuando la SMA lenta estaba por encima de la SMA rápida en la vela anterior y cae por debajo de ella en la vela actual. Cualquier posición corta abierta se cierra y se abre una posición larga con el Volume configurado.
  • Un cruce bajista ocurre cuando la SMA lenta estaba por debajo de la SMA rápida en la vela anterior y sube por encima de ella en la vela actual. Cualquier posición larga abierta se cierra y se abre una posición corta con el Volume configurado.

Reglas de salida

  • Las posiciones abiertas se cierran en el cruce opuesto como se describe arriba.
  • Take profit opcional: si TakeProfitOffset es mayor que cero, la estrategia calcula un objetivo de precio fijo desde el precio de entrada. Las operaciones largas salen cuando el precio alcanza entrada + TakeProfitOffset; las operaciones cortas salen cuando el precio alcanza entrada - TakeProfitOffset.
  • Trailing stop opcional: cuando TrailingStopOffset es mayor que cero, la estrategia espera hasta que el beneficio no realizado supere TrailingStopOffset + TrailingStepOffset. Una vez que ese umbral es cruzado, el precio de stop se ajusta para mantenerse TrailingStopOffset alejado del último cierre, pero solo si el nuevo nivel está al menos TrailingStepOffset más cerca del precio que el stop anterior. Las operaciones largas usan los mínimos para activar el stop, los cortos usan los máximos.

Parámetros

Parámetro Por defecto Descripción
CandleType Marco temporal de 15 minutos Resolución de vela usada para cálculos SMA y evaluación de señales.
FastPeriod 6 Período para la media móvil simple rápida. Debe ser > 0 y menor que SlowPeriod.
SlowPeriod 54 Período para la media móvil simple lenta. Debe ser > 0 y mayor que FastPeriod.
Volume 1 Volumen de orden usado para nuevas entradas.
TakeProfitOffset 0.001 Distancia de precio absoluta opcional para el objetivo de take profit. Establecer en 0 para deshabilitar.
TrailingStopOffset 0.005 Distancia absoluta entre el precio y el trailing stop. Establecer en 0 para deshabilitar el trailing.
TrailingStepOffset 0.0005 Movimiento favorable adicional requerido antes de que el trailing stop se mueva.

Importante: los offsets se especifican en unidades de precio absolutas. Ajústelos para que coincidan con el tamaño del tick del instrumento (por ejemplo, en EURUSD con un paso de 0.0001, los valores por defecto corresponden a 10, 50 y 5 pips respectivamente).

Notas de implementación

  • Usa el flujo de trabajo de alto nivel SubscribeCandles().Bind() según lo requieren las pautas del proyecto.
  • La salida del gráfico traza ambas SMAs y marcadores de operaciones cuando el gráfico está disponible en el entorno.
  • Las variables de estado rastrean el precio de entrada, el nivel de trailing stop y el objetivo de take profit exactamente como la versión MQL.
  • La implementación en C# aplica SlowPeriod > FastPeriod al inicio para evitar una configuración de indicador no válida.

Consejos de uso

  • Optimice el marco temporal de las velas y los períodos SMA para que coincidan con el mercado que se negocia (p.ej., períodos más cortos para futuros intradía, más largos para swing trading).
  • Convierta los offsets de pips o ticks a unidades de precio absolutas antes de ejecutar la estrategia.
  • El trailing puede desactivarse estableciendo TrailingStopOffset en cero; la estrategia entonces dependerá únicamente del cruce opuesto o del take profit opcional para las salidas.
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>
/// EMA 6/12 crossover strategy with trailing stop management.
/// </summary>
public class Ema612CrossoverStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<decimal> _takeProfitOffset;
	private readonly StrategyParam<decimal> _trailingStopOffset;
	private readonly StrategyParam<decimal> _trailingStepOffset;

	private ExponentialMovingAverage _fastSma;
	private ExponentialMovingAverage _slowSma;

	private decimal? _prevFast;
	private decimal? _prevSlow;

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;

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

	/// <summary>
	/// Fast moving average period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow moving average period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}


	/// <summary>
	/// Take profit distance in absolute price units.
	/// </summary>
	public decimal TakeProfitOffset
	{
		get => _takeProfitOffset.Value;
		set => _takeProfitOffset.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in absolute price units.
	/// </summary>
	public decimal TrailingStopOffset
	{
		get => _trailingStopOffset.Value;
		set => _trailingStopOffset.Value = value;
	}

	/// <summary>
	/// Additional distance required to move the trailing stop.
	/// </summary>
	public decimal TrailingStepOffset
	{
		get => _trailingStepOffset.Value;
		set => _trailingStepOffset.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="Ema612CrossoverStrategy"/>.
	/// </summary>
	public Ema612CrossoverStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Candle resolution", "General");
		_fastPeriod = Param(nameof(FastPeriod), 6)
			.SetGreaterThanZero()
			.SetDisplay("Fast Period", "Fast SMA length", "Moving Averages");
		_slowPeriod = Param(nameof(SlowPeriod), 54)
			.SetGreaterThanZero()
			.SetDisplay("Slow Period", "Slow SMA length", "Moving Averages");
		_takeProfitOffset = Param(nameof(TakeProfitOffset), 0.001m)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Target distance in price units", "Risk");
		_trailingStopOffset = Param(nameof(TrailingStopOffset), 0.005m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop", "Trailing stop distance", "Risk");
		_trailingStepOffset = Param(nameof(TrailingStepOffset), 0.0005m)
			.SetNotNegative()
			.SetDisplay("Trailing Step", "Additional profit required to tighten stop", "Risk");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		ResetPositionState();
		_prevFast = null;
		_prevSlow = null;
	}

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

		if (SlowPeriod <= FastPeriod)
			throw new InvalidOperationException("Slow period must be greater than fast period.");

		_fastSma = new ExponentialMovingAverage { Length = FastPeriod };
		_slowSma = new ExponentialMovingAverage { Length = SlowPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_fastSma, _slowSma, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _fastSma);
			DrawIndicator(area, _slowSma);
			DrawOwnTrades(area);
		}
	}

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

		if (!_fastSma.IsFormed || !_slowSma.IsFormed)
			return;

		var bullishCross = false;
		var bearishCross = false;

		if (_prevFast.HasValue && _prevSlow.HasValue)
		{
			// Detect crossovers using previous candle values.
			bullishCross = _prevSlow > _prevFast && slow < fast;
			bearishCross = _prevSlow < _prevFast && slow > fast;
		}

		HandleExistingPosition(candle, bullishCross, bearishCross);

		if (Position == 0)
		{
			if (bullishCross)
			{
				// Slow MA crossed below the fast MA - go long.
				EnterLong(candle);
			}
			else if (bearishCross)
			{
				// Slow MA crossed above the fast MA - go short.
				EnterShort(candle);
			}
		}

		_prevFast = fast;
		_prevSlow = slow;
	}

	private void HandleExistingPosition(ICandleMessage candle, bool bullishCross, bool bearishCross)
	{
		if (Position > 0)
		{
			// Update trailing stop for the long position before evaluating exits.
			UpdateLongTrailing(candle);

			var exit = bearishCross;
			if (!exit && _takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				// Price reached the take profit objective.
				exit = true;
			}

			if (!exit && _stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				// Price retraced to the trailing stop.
				exit = true;
			}

			if (exit)
			{
				SellMarket(Position);
				ResetPositionState();
			}
		}
		else if (Position < 0)
		{
			// Update trailing stop for the short position before evaluating exits.
			UpdateShortTrailing(candle);

			var exit = bullishCross;
			if (!exit && _takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				// Price reached the take profit objective for the short trade.
				exit = true;
			}

			if (!exit && _stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				// Price rallied back to the trailing stop level.
				exit = true;
			}

			if (exit)
			{
				BuyMarket(Math.Abs(Position));
				ResetPositionState();
			}
		}
	}

	private void EnterLong(ICandleMessage candle)
	{
		BuyMarket(Volume);
		_entryPrice = candle.ClosePrice;
		_takeProfitPrice = TakeProfitOffset > 0m ? candle.ClosePrice + TakeProfitOffset : null;
		_stopPrice = null;
	}

	private void EnterShort(ICandleMessage candle)
	{
		SellMarket(Volume);
		_entryPrice = candle.ClosePrice;
		_takeProfitPrice = TakeProfitOffset > 0m ? candle.ClosePrice - TakeProfitOffset : null;
		_stopPrice = null;
	}

	private void UpdateLongTrailing(ICandleMessage candle)
	{
		if (TrailingStopOffset <= 0m || !_entryPrice.HasValue)
			return;

		var gain = candle.ClosePrice - _entryPrice.Value;
		var triggerDistance = TrailingStopOffset + TrailingStepOffset;

		if (gain <= triggerDistance)
			return;

		var candidate = candle.ClosePrice - TrailingStopOffset;
		var minAdvance = TrailingStepOffset <= 0m ? 0m : TrailingStepOffset;

		if (!_stopPrice.HasValue || candidate - _stopPrice.Value > minAdvance)
		{
			// Move stop loss closer only when price progressed enough.
			_stopPrice = candidate;
		}
	}

	private void UpdateShortTrailing(ICandleMessage candle)
	{
		if (TrailingStopOffset <= 0m || !_entryPrice.HasValue)
			return;

		var gain = _entryPrice.Value - candle.ClosePrice;
		var triggerDistance = TrailingStopOffset + TrailingStepOffset;

		if (gain <= triggerDistance)
			return;

		var candidate = candle.ClosePrice + TrailingStopOffset;
		var minAdvance = TrailingStepOffset <= 0m ? 0m : TrailingStepOffset;

		if (!_stopPrice.HasValue || _stopPrice.Value - candidate > minAdvance)
		{
			// Move stop loss for the short only after sufficient favorable movement.
			_stopPrice = candidate;
		}
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
	}
}