Ver en GitHub

Estrategia GoldWarrior02b

Estrategia algorítmica convertida del asesor experto de MetaTrader GoldWarrior02b. Combina un medidor de impulso, el Commodity Channel Index (CCI) y un simple detector de oscilación ZigZag para operar cerca del final de cada bloque de 15 minutos.

La implementación se dirige a la API de alto nivel de StockSharp y se centra en posiciones netas. El hedging multinivel del script original no es compatible porque StockSharp trabaja con posiciones neteadas.

Concepto

  • Usar un indicador de impulso personalizado que promedia la diferencia entre los precios de apertura y cierre de las velas.
  • Evaluar los valores del CCI para detectar reversiones de sobrecompra/sobreventa y picos de momentum fuertes.
  • Derivar una dirección de oscilación ZigZag de los máximos y mínimos recientes para evitar operar contra el movimiento dominante.
  • Solo evaluar señales durante los segundos finales (>= 45s) de los minutos 14, 29, 44 y 59.
  • Aplicar gestión de riesgo dinámica con stop-loss, take-profit, trailing-stop y un objetivo de beneficio global.

Reglas de Entrada

Una operación se considera solo si no hay posición abierta y la vela actual cierra dentro de la ventana de tiempo descrita anteriormente.

Configuración Larga

  • La oscilación ZigZag apunta hacia abajo (el mínimo reciente es menor que el anterior).
  • Ya sea:
    • El CCI sube por encima de su lectura anterior mientras el CCI anterior estaba por debajo de -50, el CCI actual por debajo de -30, el impulso se vuelve positivo y el impulso anterior era negativo.
    • O el CCI cae por debajo de -200, el CCI anterior era aún más bajo, el impulso permanece por debajo del umbral positivo y el impulso anterior es más débil que el valor actual.

Configuración Corta

  • La oscilación ZigZag apunta hacia arriba (el máximo reciente es mayor que el anterior).
  • Ya sea:
    • El CCI cae por debajo de su lectura anterior mientras el CCI anterior estaba por encima de 50, el CCI actual por encima de 30, el impulso se vuelve negativo y el impulso anterior era positivo.
    • O el CCI supera 200, el CCI anterior era más alto, el impulso se mantiene por encima del umbral negativo y el impulso anterior es más fuerte que el valor actual.

Si el impulso anterior permanece entre los umbrales de compra y venta configurados, las señales se ignoran.

Reglas de Salida

  • Stop-loss: cierra la posición cuando el precio cruza la distancia de stop desde el precio de entrada.
  • Take-profit: cierra después de alcanzar la distancia de beneficio configurada.
  • Trailing stop: una vez que el precio avanza por (TrailingStop + TrailingStep) puntos, el nivel de trailing sigue al precio a una distancia de TrailingStop puntos. Cruzar el nivel de trailing sale de la operación.
  • Objetivo de beneficio global: cierra la posición cuando el PnL no realizado supera el importe especificado (en moneda de la cuenta).

Parámetros

Nombre Descripción Valor predeterminado
BaseVolume Tamaño de operación para entradas. 0.1
StopLossPoints Distancia del stop en puntos. 100
TakeProfitPoints Distancia del take-profit en puntos. 150
TrailingStopPoints Distancia base del trailing stop. 5
TrailingStepPoints Distancia adicional antes de que el trailing stop se active. 5
ImpulsePeriod Período para los cálculos del CCI e impulso. 21
ZigZagDepth Mínimo de barras entre nuevas oscilaciones ZigZag. 12
ZigZagDeviation Movimiento de precio mínimo (en puntos) para confirmar una oscilación. 5
ZigZagBackstep Mínimo de barras antes de aceptar una nueva oscilación. 3
ProfitTarget Umbral de beneficio no realizado para cerrar todas las posiciones. 300
ImpulseSellThreshold Umbral de impulso para cortos (típicamente negativo). -30
ImpulseBuyThreshold Umbral de impulso para largos (típicamente positivo). 30
CandleType Marco temporal usado para los cálculos. Marco temporal de 5 minutos

Notas

  • El indicador de impulso es una media móvil de la diferencia entre los valores de apertura y cierre de las velas escalada por el paso de precio del instrumento.
  • Los cálculos de trailing y PnL se basan en PriceStep y StepPrice del instrumento para convertir distancias en puntos a moneda de la cuenta.
  • El asesor experto original escala tamaños de posición y despliega niveles de hedging. Este puerto de StockSharp mantiene una única posición neta por instrumento, correspondiendo con el modelo de ejecución de StockSharp.
  • Para replicar el comportamiento original más fielmente, considera habilitar una suscripción de velas de 15 minutos y asegurarte de que la latencia de datos de ticks permita la ejecución poco después del timestamp de cierre.

Esta muestra es para fines educativos. Antes de ejecutar en mercados en vivo, valide la estrategia bajo condiciones realistas de datos, latencia y comisiones.

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>
/// Port of the MetaTrader GoldWarrior02b expert advisor adapted for StockSharp.
/// Combines CCI, an impulse gauge and a ZigZag swing detector to trade near the end of 15 minute blocks.
/// </summary>
public class GoldWarrior02bStrategy : Strategy
{
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<int> _impulsePeriod;
	private readonly StrategyParam<int> _zigZagDepth;
	private readonly StrategyParam<decimal> _zigZagDeviation;
	private readonly StrategyParam<int> _zigZagBackstep;
	private readonly StrategyParam<decimal> _profitTarget;
	private readonly StrategyParam<decimal> _impulseSellThreshold;
	private readonly StrategyParam<decimal> _impulseBuyThreshold;
	private readonly StrategyParam<DataType> _candleType;

	private CommodityChannelIndex _cci = null!;
	private ImpulseIndicator _impulse = null!;

	private decimal? _lastZigZag;
	private decimal? _previousZigZag;
	private int _searchDirection;
	private decimal? _currentExtreme;
	private int _barsSinceExtreme;

	private decimal _previousCci;
	private decimal _previousImpulse;
	private bool _hasPreviousCci;
	private bool _hasPreviousImpulse;

	private DateTimeOffset _lastTradeTime;
	private decimal _entryPrice;
	private decimal _trailingStopPrice;
	private bool _trailingActive;
	private decimal _maxPriceSinceEntry;
	private decimal _minPriceSinceEntry;

	/// <summary>
	/// Base trading volume.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

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

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

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

	/// <summary>
	/// Additional offset before activating the trailing stop.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Period used both for CCI and impulse calculations.
	/// </summary>
	public int ImpulsePeriod
	{
		get => _impulsePeriod.Value;
		set => _impulsePeriod.Value = value;
	}

	/// <summary>
	/// Minimum bars between ZigZag turning points.
	/// </summary>
	public int ZigZagDepth
	{
		get => _zigZagDepth.Value;
		set => _zigZagDepth.Value = value;
	}

	/// <summary>
	/// Minimum price deviation to confirm a new ZigZag swing.
	/// </summary>
	public decimal ZigZagDeviation
	{
		get => _zigZagDeviation.Value;
		set => _zigZagDeviation.Value = value;
	}

	/// <summary>
	/// Minimum number of bars before accepting a new swing.
	/// </summary>
	public int ZigZagBackstep
	{
		get => _zigZagBackstep.Value;
		set => _zigZagBackstep.Value = value;
	}

	/// <summary>
	/// Profit target that forces an early exit from open positions.
	/// </summary>
	public decimal ProfitTarget
	{
		get => _profitTarget.Value;
		set => _profitTarget.Value = value;
	}

	/// <summary>
	/// Threshold applied to the impulse gauge before opening shorts.
	/// </summary>
	public decimal ImpulseSellThreshold
	{
		get => _impulseSellThreshold.Value;
		set => _impulseSellThreshold.Value = value;
	}

	/// <summary>
	/// Threshold applied to the impulse gauge before opening longs.
	/// </summary>
	public decimal ImpulseBuyThreshold
	{
		get => _impulseBuyThreshold.Value;
		set => _impulseBuyThreshold.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 the strategy.
	/// </summary>
	public GoldWarrior02bStrategy()
	{
		_baseVolume = Param(nameof(BaseVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Volume", "Base trade size", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 100m)
		.SetNotNegative()
		.SetDisplay("Stop Loss", "Stop-loss distance in points", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 150m)
		.SetNotNegative()
		.SetDisplay("Take Profit", "Take-profit distance in points", "Risk");

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 5m)
		.SetNotNegative()
		.SetDisplay("Trailing Stop", "Trailing stop distance in points", "Risk");

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m)
		.SetNotNegative()
		.SetDisplay("Trailing Step", "Extra distance before trailing activates", "Risk");

		_impulsePeriod = Param(nameof(ImpulsePeriod), 21)
		.SetGreaterThanZero()
		.SetDisplay("Impulse Period", "Period for CCI and impulse averages", "Indicators");

		_zigZagDepth = Param(nameof(ZigZagDepth), 12)
		.SetGreaterThanZero()
		.SetDisplay("ZigZag Depth", "Minimum bars between swings", "Indicators");

		_zigZagDeviation = Param(nameof(ZigZagDeviation), 5m)
		.SetGreaterThanZero()
		.SetDisplay("ZigZag Deviation", "Required price move in points", "Indicators");

		_zigZagBackstep = Param(nameof(ZigZagBackstep), 3)
		.SetGreaterThanZero()
		.SetDisplay("ZigZag Backstep", "Bars before confirming a new swing", "Indicators");

		_profitTarget = Param(nameof(ProfitTarget), 300m)
		.SetNotNegative()
		.SetDisplay("Profit Target", "Close all profit in account currency", "Risk");

		_impulseSellThreshold = Param(nameof(ImpulseSellThreshold), -30m)
		.SetDisplay("Impulse Sell", "Impulse threshold for shorts", "Indicators");

		_impulseBuyThreshold = Param(nameof(ImpulseBuyThreshold), 30m)
		.SetDisplay("Impulse Buy", "Impulse threshold for longs", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
		.SetDisplay("Candle Type", "Working timeframe", "General");
	}

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

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

		_cci?.Reset();
		_impulse?.Reset();

		_lastZigZag = null;
		_previousZigZag = null;
		_searchDirection = 1;
		_currentExtreme = null;
		_barsSinceExtreme = 0;

		_previousCci = 0m;
		_previousImpulse = 0m;
		_hasPreviousCci = false;
		_hasPreviousImpulse = false;

		_lastTradeTime = DateTimeOffset.MinValue;
		ResetPositionState();
	}

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

		Volume = BaseVolume;

		_cci = new CommodityChannelIndex { Length = ImpulsePeriod };
		_impulse = new ImpulseIndicator
		{
			Length = ImpulsePeriod,
			PriceStep = GetPriceStep()
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(_cci, _impulse, ProcessCandle)
		.Start();

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

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

	
		_impulse.PriceStep = GetPriceStep();

		if (!_cci.IsFormed || !_impulse.IsFormed)
		{
			_previousCci = cciValue;
			_previousImpulse = impulseValue;
			_hasPreviousCci = true;
			_hasPreviousImpulse = true;
			UpdateZigZag(candle);
			return;
		}

		UpdateZigZag(candle);

		var hasZigZag = _lastZigZag.HasValue && _previousZigZag.HasValue;
		var zigZagUp = hasZigZag && _lastZigZag.Value > _previousZigZag.Value;
		var zigZagDown = hasZigZag && _lastZigZag.Value < _previousZigZag.Value;

		if (!_hasPreviousCci || !_hasPreviousImpulse)
		{
			_previousCci = cciValue;
			_previousImpulse = impulseValue;
			_hasPreviousCci = true;
			_hasPreviousImpulse = true;
			return;
		}

		var now = candle.CloseTime;
		if ((now - _lastTradeTime).TotalSeconds < 15)
		{
			_previousCci = cciValue;
			_previousImpulse = impulseValue;
			return;
		}

		var sellCondition1 = cciValue < _previousCci && _previousCci > 20m && impulseValue < 0m;
		var sellCondition2 = cciValue > 100m && _previousCci > cciValue;
		var buyCondition1 = cciValue > _previousCci && _previousCci < -20m && impulseValue > 0m;
		var buyCondition2 = cciValue < -100m && _previousCci < cciValue;

		var sellSignal = hasZigZag && zigZagUp && (sellCondition1 || sellCondition2);
		var buySignal = hasZigZag && zigZagDown && (buyCondition1 || buyCondition2);

		if (!hasZigZag || Position != 0)
		{
			sellSignal = false;
			buySignal = false;
		}

		if (Position == 0 && AllowEntryTime(now))
		{
			if (sellSignal)
			OpenShort(candle, BaseVolume);
			else if (buySignal)
			OpenLong(candle, BaseVolume);
		}

		if (Position != 0)
		{
			HandleActivePosition(candle, now);
		}

		_previousCci = cciValue;
		_previousImpulse = impulseValue;
	}

	private void HandleActivePosition(ICandleMessage candle, DateTimeOffset now)
	{
		var step = GetPriceStep();
		var stepPrice = GetStepPrice(step);

		var stopLossDistance = StopLossPoints * step;
		var takeProfitDistance = TakeProfitPoints * step;
		var trailingStopDistance = TrailingStopPoints * step;
		var trailingStepDistance = TrailingStepPoints * step;

		if (Position > 0)
		{
			_maxPriceSinceEntry = Math.Max(_maxPriceSinceEntry, candle.HighPrice);

			if (stopLossDistance > 0m && candle.LowPrice <= _entryPrice - stopLossDistance)
			{
				SellMarket(Position);
				_lastTradeTime = now;
				ResetPositionState();
				return;
			}

			if (takeProfitDistance > 0m && candle.HighPrice >= _entryPrice + takeProfitDistance)
			{
				SellMarket(Position);
				_lastTradeTime = now;
				ResetPositionState();
				return;
			}

			if (trailingStopDistance > 0m)
			{
				var move = candle.ClosePrice - _entryPrice;
				if (move >= trailingStopDistance + trailingStepDistance)
				{
					var newTrail = candle.ClosePrice - trailingStopDistance;
					if (!_trailingActive || newTrail > _trailingStopPrice)
					{
						_trailingStopPrice = newTrail;
						_trailingActive = true;
					}
				}

				if (_trailingActive && candle.LowPrice <= _trailingStopPrice)
				{
					SellMarket(Position);
					_lastTradeTime = now;
					ResetPositionState();
					return;
				}
			}
		}
		else if (Position < 0)
		{
			_minPriceSinceEntry = Math.Min(_minPriceSinceEntry, candle.LowPrice);

			if (stopLossDistance > 0m && candle.HighPrice >= _entryPrice + stopLossDistance)
			{
				BuyMarket(-Position);
				_lastTradeTime = now;
				ResetPositionState();
				return;
			}

			if (takeProfitDistance > 0m && candle.LowPrice <= _entryPrice - takeProfitDistance)
			{
				BuyMarket(-Position);
				_lastTradeTime = now;
				ResetPositionState();
				return;
			}

			if (trailingStopDistance > 0m)
			{
				var move = _entryPrice - candle.ClosePrice;
				if (move >= trailingStopDistance + trailingStepDistance)
				{
					var newTrail = candle.ClosePrice + trailingStopDistance;
					if (!_trailingActive || newTrail < _trailingStopPrice)
					{
						_trailingStopPrice = newTrail;
						_trailingActive = true;
					}
				}

				if (_trailingActive && candle.HighPrice >= _trailingStopPrice)
				{
					BuyMarket(-Position);
					_lastTradeTime = now;
					ResetPositionState();
					return;
				}
			}
		}

		var currentPnL = CalculateOpenPnL(candle.ClosePrice, step, stepPrice);
		if (ProfitTarget > 0m && currentPnL >= ProfitTarget)
		{
			if (Position > 0)
			SellMarket(Position);
			else if (Position < 0)
			BuyMarket(-Position);

			_lastTradeTime = now;
			ResetPositionState();
			return;
		}
	}

	private decimal CalculateOpenPnL(decimal closePrice, decimal step, decimal stepPrice)
	{
		if (Position == 0)
		return 0m;

		if (step <= 0m)
		step = 1m;
		if (stepPrice <= 0m)
		stepPrice = step;

		if (Position > 0)
		{
			var diff = closePrice - _entryPrice;
			return diff / step * stepPrice * Position;
		}
		else
		{
			var diff = _entryPrice - closePrice;
			return diff / step * stepPrice * -Position;
		}
	}

	private void OpenLong(ICandleMessage candle, decimal volume)
	{
		BuyMarket(volume);
		_entryPrice = candle.ClosePrice;
		_maxPriceSinceEntry = candle.ClosePrice;
		_minPriceSinceEntry = candle.ClosePrice;
		_trailingActive = false;
		_trailingStopPrice = 0m;
		_lastTradeTime = candle.CloseTime;
	}

	private void OpenShort(ICandleMessage candle, decimal volume)
	{
		SellMarket(volume);
		_entryPrice = candle.ClosePrice;
		_maxPriceSinceEntry = candle.ClosePrice;
		_minPriceSinceEntry = candle.ClosePrice;
		_trailingActive = false;
		_trailingStopPrice = 0m;
		_lastTradeTime = candle.CloseTime;
	}

	private void ResetPositionState()
	{
		_entryPrice = 0m;
		_maxPriceSinceEntry = 0m;
		_minPriceSinceEntry = 0m;
		_trailingActive = false;
		_trailingStopPrice = 0m;
	}

	private bool AllowEntryTime(DateTimeOffset time)
	{
		return true;
	}

	private void UpdateZigZag(ICandleMessage candle)
	{
		var step = GetPriceStep();
		var deviation = ZigZagDeviation * step;
		var minBars = Math.Max(1, Math.Max(ZigZagDepth, ZigZagBackstep));

		if (_currentExtreme is null)
		{
			_currentExtreme = _searchDirection > 0 ? candle.HighPrice : candle.LowPrice;
			_barsSinceExtreme = 0;
			return;
		}

		if (_searchDirection > 0)
		{
			if (candle.HighPrice > _currentExtreme.Value)
			{
				_currentExtreme = candle.HighPrice;
				_barsSinceExtreme = 0;
			}
			else
			{
				_barsSinceExtreme++;
			}

			var drop = _currentExtreme.Value - candle.LowPrice;
			if (drop >= deviation && _barsSinceExtreme >= minBars)
			{
				_previousZigZag = _lastZigZag;
				_lastZigZag = _currentExtreme;
				_searchDirection = -1;
				_currentExtreme = candle.LowPrice;
				_barsSinceExtreme = 0;
			}
		}
		else
		{
			if (candle.LowPrice < _currentExtreme.Value)
			{
				_currentExtreme = candle.LowPrice;
				_barsSinceExtreme = 0;
			}
			else
			{
				_barsSinceExtreme++;
			}

			var rise = candle.HighPrice - _currentExtreme.Value;
			if (rise >= deviation && _barsSinceExtreme >= minBars)
			{
				_previousZigZag = _lastZigZag;
				_lastZigZag = _currentExtreme;
				_searchDirection = 1;
				_currentExtreme = candle.HighPrice;
				_barsSinceExtreme = 0;
			}
		}
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep ?? 1m;
		return step > 0m ? step : 1m;
	}

	private decimal GetStepPrice(decimal step)
	{
		var stepPrice = step;
		return stepPrice > 0m ? stepPrice : step;
	}

	private sealed class ImpulseIndicator : BaseIndicator
	{
		public int Length { get; set; } = 21;
		public decimal PriceStep { get; set; } = 1m;

		private readonly Queue<decimal> _buffer = new();
		private decimal _sum;

		protected override IIndicatorValue OnProcess(IIndicatorValue input)
		{
			var candle = input.GetValue<ICandleMessage>();
			var step = PriceStep > 0m ? PriceStep : 1m;
			var value = (candle.OpenPrice - candle.ClosePrice) / step;

			_buffer.Enqueue(value);
			_sum += value;

			if (_buffer.Count > Length)
			_sum -= _buffer.Dequeue();

			if (_buffer.Count < Length)
			{
				IsFormed = false;
				return new DecimalIndicatorValue(this, 0m, input.Time);
			}

			IsFormed = true;
			var average = _sum / Length;
			return new DecimalIndicatorValue(this, average, input.Time);
		}

		public override void Reset()
		{
			base.Reset();
			_buffer.Clear();
			_sum = 0m;
		}
	}
}