Ver en GitHub

Estrategia EMA WMA Contrarian

Sistema contrario de cruce que compara una media móvil exponencial (EMA) y una media móvil ponderada (WMA) construidas sobre precios de apertura de velas. Cuando la EMA rápida cae por debajo de la WMA, la estrategia compra apostando a un retorno. Cuando la EMA sube de nuevo por encima de la WMA, entra en corto. El tamaño de la operación se deriva del porcentaje de riesgo configurado y la distancia al stop protector, mientras que los niveles opcionales de stop-loss, take-profit y trailing stop mantienen la exposición bajo control.

Detalles

  • Criterios de entrada:
    • Largo: EMA(Apertura) cruza de arriba hacia abajo de la WMA(Apertura)
    • Corto: EMA(Apertura) cruza de abajo hacia arriba de la WMA(Apertura)
  • Largo/Corto: Ambas direcciones
  • Criterios de salida:
    • Stop-loss fijo en pasos de precio
    • Take-profit fijo en pasos de precio
    • Trailing stop que avanza después de que el precio se mueve TrailingStopPoints + TrailingStepPoints
    • El cruce opuesto cierra la posición actual y abre la nueva
  • Stops: Stop-loss, take-profit y trailing stop
  • Valores predeterminados:
    • EmaPeriod = 28
    • WmaPeriod = 8
    • StopLossPoints = 50m
    • TakeProfitPoints = 50m
    • TrailingStopPoints = 50m
    • TrailingStepPoints = 10m
    • RiskPercent = 10m
    • BaseVolume = 1m
    • CandleType = TimeSpan.FromMinutes(1).TimeFrame()
  • Filtros:
    • Categoría: Media Móvil, Contrarian
    • Dirección: Largo y Corto
    • Indicadores: EMA (apertura), WMA (apertura)
    • Stops: Sí (stop fijo, trailing)
    • Complejidad: Intermedio
    • Marco temporal: Intradía (predeterminado 1 minuto)
    • Estacionalidad: Ninguno
    • Redes neuronales: No
    • Divergencia: No
    • Nivel de riesgo: Medio

Parámetros

Parámetro Descripción
EmaPeriod, WmaPeriod Períodos de retrospección para la EMA y WMA calculadas en las aperturas de velas.
StopLossPoints, TakeProfitPoints Distancia en pasos de precio para colocar el stop-loss protector y el objetivo de ganancia.
TrailingStopPoints Distancia entre el precio y el trailing stop una vez activado.
TrailingStepPoints Movimiento favorable adicional requerido antes de que el trailing stop suba/baje. Debe ser positivo cuando el trailing está habilitado.
RiskPercent Porcentaje del capital del portafolio arriesgado por operación. El tamaño de posición se calcula como RiskPercent / (StopLossPoints * PriceStep).
BaseVolume Tamaño mínimo de operación utilizado cuando el dimensionamiento basado en riesgo no se puede determinar.
CandleType Tipo de datos de vela para cálculos (predeterminado 1 minuto).

Notas

  • Ambas medias móviles consumen precios de apertura de velas, reflejando el asesor experto original de MetaTrader.
  • Los trailing stops solo se activan después de que el precio se mueve al menos TrailingStopPoints + TrailingStepPoints a favor de la operación, replicando la lógica heredada.
  • Si TrailingStopPoints está configurado mientras TrailingStepPoints es cero o negativo, la estrategia se detiene inmediatamente para evitar un comportamiento de trailing inconsistente.
  • El dimensionamiento basado en riesgo recurre a BaseVolume si el valor del portafolio, el paso de precio o la distancia del stop no están disponibles.
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>
/// Contrarian crossover between EMA and WMA calculated on candle open prices.
/// Opens a long position when EMA crosses below WMA and a short position on the opposite cross.
/// Supports fixed stop-loss, take-profit, and trailing stop management plus risk-based position sizing.
/// </summary>
public class EmaWmaContrarianStrategy : Strategy
{
	private readonly StrategyParam<int> _emaPeriod;
	private readonly StrategyParam<int> _wmaPeriod;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _ema;
	private WeightedMovingAverage _wma;
	private bool _hasPrevious;
	private decimal _previousEma;
	private decimal _previousWma;

	private decimal? _entryPrice;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;

	/// <summary>
	/// EMA period.
	/// </summary>
	public int EmaPeriod
	{
		get => _emaPeriod.Value;
		set => _emaPeriod.Value = value;
	}

	/// <summary>
	/// WMA period.
	/// </summary>
	public int WmaPeriod
	{
		get => _wmaPeriod.Value;
		set => _wmaPeriod.Value = value;
	}

	/// <summary>
	/// Stop-loss in price steps.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit in price steps.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in price steps.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop step in price steps.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Risk percentage used for position sizing.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Minimum contract volume used when risk sizing cannot be applied.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of <see cref="EmaWmaContrarianStrategy"/>.
	/// </summary>
	public EmaWmaContrarianStrategy()
	{
		_emaPeriod = Param(nameof(EmaPeriod), 28)
			.SetGreaterThanZero()
			.SetDisplay("EMA Period", "EMA length calculated on candle open prices", "Indicators")
			
			.SetOptimize(10, 60, 2);

		_wmaPeriod = Param(nameof(WmaPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("WMA Period", "WMA length calculated on candle open prices", "Indicators")
			
			.SetOptimize(4, 40, 2);

		_stopLossPoints = Param(nameof(StopLossPoints), 50m)
			.SetDisplay("Stop Loss (points)", "Stop-loss distance expressed in price steps", "Risk")
			
			.SetOptimize(10m, 150m, 10m);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 50m)
			.SetDisplay("Take Profit (points)", "Take-profit distance expressed in price steps", "Risk")
			
			.SetOptimize(10m, 200m, 10m);

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 50m)
			.SetDisplay("Trailing Stop (points)", "Trailing stop distance expressed in price steps", "Risk")
			
			.SetOptimize(10m, 150m, 10m);

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 10m)
			.SetDisplay("Trailing Step (points)", "Minimal favorable move before the trailing stop is advanced", "Risk")
			
			.SetOptimize(5m, 50m, 5m);

		_riskPercent = Param(nameof(RiskPercent), 10m)
			.SetDisplay("Risk Percent", "Portfolio percentage risked per trade", "Position Sizing")
			
			.SetOptimize(2m, 20m, 2m);

		_baseVolume = Param(nameof(BaseVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Base Volume", "Fallback volume when risk sizing is unavailable", "Position Sizing");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle type used for indicators", "General");
	}

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

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

		_ema = null;
		_wma = null;
		_hasPrevious = false;
		_previousEma = 0m;
		_previousWma = 0m;
		ClearPositionState();
	}

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

		// Validate trailing configuration to match original expert advisor behaviour.
		if (TrailingStopPoints > 0 && TrailingStepPoints <= 0)
		{
			Stop();
			return;
		}

		_ema = new ExponentialMovingAverage { Length = EmaPeriod };
		_wma = new WeightedMovingAverage { Length = WmaPeriod };

		// Subscribe to candles and connect indicators.
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle);
		subscription.Start();

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Process only finished candles to avoid intrabar noise.
		if (candle.State != CandleStates.Finished)
			return;

		if (_ema == null || _wma == null)
			return;

		// Evaluate protective logic before generating new signals.
		ManageActivePosition(candle);

		var emaValue = _ema.Process(new DecimalIndicatorValue(_ema, candle.OpenPrice, candle.OpenTime) { IsFinal = true });
		var wmaValue = _wma.Process(new DecimalIndicatorValue(_wma, candle.OpenPrice, candle.OpenTime) { IsFinal = true });

		// Ensure indicators produced valid values.
		if (emaValue.IsEmpty || wmaValue.IsEmpty || !_ema.IsFormed || !_wma.IsFormed)
			return;

		var ema = emaValue.ToDecimal();
		var wma = wmaValue.ToDecimal();

		if (!_hasPrevious)
		{
			_previousEma = ema;
			_previousWma = wma;
			_hasPrevious = true;
			return;
		}

		// Detect crossovers on open-price moving averages.
		var buySignal = ema < wma && _previousEma > _previousWma;
		var sellSignal = ema > wma && _previousEma < _previousWma;

		if (buySignal)
		{
			EnterLong(candle);
		}
		else if (sellSignal)
		{
			EnterShort(candle);
		}

		_previousEma = ema;
		_previousWma = wma;
	}

	private void ManageActivePosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			// Manage long position exits.
			var currentPrice = candle.ClosePrice;

			// Exit long when take-profit is reached.
			if (_takeProfitPrice is decimal tp && currentPrice >= tp)
			{
				SellMarket(Position);
				ClearPositionState();
				return;
			}

			// Exit long when stop-loss is hit.
			if (_stopLossPrice is decimal sl && currentPrice <= sl)
			{
				SellMarket(Position);
				ClearPositionState();
				return;
			}

			// Advance trailing stop for long trades.
			if (_entryPrice is decimal entry)
				UpdateTrailingForLong(currentPrice, entry);
		}
		else if (Position < 0)
		{
			// Manage short position exits.
			var currentPrice = candle.ClosePrice;

			// Exit short when take-profit is reached.
			if (_takeProfitPrice is decimal tp && currentPrice <= tp)
			{
				BuyMarket(Math.Abs(Position));
				ClearPositionState();
				return;
			}

			// Exit short when stop-loss is hit.
			if (_stopLossPrice is decimal sl && currentPrice >= sl)
			{
				BuyMarket(Math.Abs(Position));
				ClearPositionState();
				return;
			}

			// Advance trailing stop for short trades.
			if (_entryPrice is decimal entry)
				UpdateTrailingForShort(currentPrice, entry);
		}
		else
		{
			ClearPositionState();
		}
	}

	private void EnterLong(ICandleMessage candle)
	{
		var entryPrice = candle.ClosePrice;
		var volume = CalculateTradeVolume();

		if (volume <= 0)
			return;

		if (Position < 0)
		{
			// Close an existing short position before opening a new long.
			BuyMarket(Math.Abs(Position));
			ClearPositionState();
		}

		// Open the new long trade.
		BuyMarket(volume);
		SetupRiskLevels(entryPrice, true);
	}

	private void EnterShort(ICandleMessage candle)
	{
		var entryPrice = candle.ClosePrice;
		var volume = CalculateTradeVolume();

		if (volume <= 0)
			return;

		if (Position > 0)
		{
			// Close an existing long position before opening a new short.
			SellMarket(Position);
			ClearPositionState();
		}

		// Open the new short trade.
		SellMarket(volume);
		SetupRiskLevels(entryPrice, false);
	}

	private void SetupRiskLevels(decimal entryPrice, bool isLong)
	{
		var priceStep = Security?.PriceStep ?? 1m;
		var stopDistance = StopLossPoints > 0 ? StopLossPoints * priceStep : (decimal?)null;
		var takeProfitDistance = TakeProfitPoints > 0 ? TakeProfitPoints * priceStep : (decimal?)null;

		// Remember entry price for managing exits.
		_entryPrice = entryPrice;
		_stopLossPrice = stopDistance.HasValue
			? isLong ? entryPrice - stopDistance.Value : entryPrice + stopDistance.Value
			: null;

		_takeProfitPrice = takeProfitDistance.HasValue
			? isLong ? entryPrice + takeProfitDistance.Value : entryPrice - takeProfitDistance.Value
			: null;
	}

	private decimal CalculateTradeVolume()
	{
		// Default to configured base volume.
		var volume = BaseVolume;
		var portfolioValue = Portfolio?.CurrentValue ?? 0m;
		var priceStep = Security?.PriceStep ?? 1m;
		var stopDistance = StopLossPoints * priceStep;

		// Risk-based sizing uses stop distance to allocate capital.
		if (RiskPercent > 0 && portfolioValue > 0 && stopDistance > 0)
		{
			var riskCapital = portfolioValue * (RiskPercent / 100m);
			if (riskCapital > 0)
			{
				var rawVolume = riskCapital / stopDistance;
				var adjusted = AdjustVolume(rawVolume);
				if (adjusted > 0)
					volume = adjusted;
			}
		}

		return volume;
	}

	private decimal AdjustVolume(decimal volume)
	{
		// Align volume with instrument volume step.
		var step = Security?.VolumeStep ?? 1m;
		if (step <= 0)
			step = 1m;

		var adjusted = Math.Floor(volume / step) * step;
		if (adjusted <= 0)
			adjusted = step;

		var minVolume = Security?.VolumeStep ?? step;
		if (minVolume > 0 && adjusted < minVolume)
			adjusted = minVolume;

		return adjusted;
	}

	private void UpdateTrailingForLong(decimal currentPrice, decimal entryPrice)
	{
		if (TrailingStopPoints <= 0)
			return;

		var priceStep = Security?.PriceStep ?? 1m;
		var trailingDistance = TrailingStopPoints * priceStep;
		var trailingStep = TrailingStepPoints * priceStep;

		// Trailing stop only applies after sufficient favorable movement.
		if (currentPrice - entryPrice <= trailingDistance + trailingStep)
			return;

		var comparisonLevel = currentPrice - (trailingDistance + trailingStep);
		// Raise stop-loss closer to current price.
		if (_stopLossPrice is not decimal existing || existing < comparisonLevel)
			_stopLossPrice = currentPrice - trailingDistance;
	}

	private void UpdateTrailingForShort(decimal currentPrice, decimal entryPrice)
	{
		if (TrailingStopPoints <= 0)
			return;

		var priceStep = Security?.PriceStep ?? 1m;
		var trailingDistance = TrailingStopPoints * priceStep;
		var trailingStep = TrailingStepPoints * priceStep;

		// Trailing stop only applies after sufficient favorable movement.
		if (entryPrice - currentPrice <= trailingDistance + trailingStep)
			return;

		var comparisonLevel = currentPrice + trailingDistance + trailingStep;
		// Lower stop-loss toward market for short trades.
		if (_stopLossPrice is not decimal existing || existing > comparisonLevel)
			_stopLossPrice = currentPrice + trailingDistance;
	}

	private void ClearPositionState()
	{
		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}
}