Ver en GitHub

Estrategia Pipsover Chaikin Hedge

Descripción general

Esta estrategia reproduce el asesor experto "Pipsover 2" de MetaTrader dentro de StockSharp. Busca condiciones de sobreventa o sobrecompra con el oscilador Chaikin mientras el precio perfora una media móvil y utiliza el cuerpo de la vela anterior para confirmar la reversión. El port de StockSharp mantiene la lógica de cobertura discrecional del código original: cuando aparece una señal opuesta mientras ya hay una posición, la estrategia inmediatamente revierte la exposición neta para seguir el nuevo sesgo.

Indicadores y datos

  • Oscilador Chaikin: construido desde una línea de Acumulación/Distribución suavizada por dos medias móviles. Ambas medias son configurables y coinciden con la implementación de MetaTrader (simple, exponencial, suavizada o ponderada).
  • Media móvil de precio: longitud, desplazamiento y tipo configurables. Actúa como el ancla de reversión a la media que los máximos o mínimos de la vela anterior deben perforar.
  • Marco temporal: la estrategia se suscribe a un único flujo de velas elegido a través del parámetro CandleType.

Lógica de trading

  1. Trabajar solo con velas terminadas. El cuerpo de la vela anterior (cierre vs. apertura) proporciona el sesgo direccional.
  2. Leer el valor del oscilador Chaikin de la vela anterior. Los valores negativos grandes señalan sobreventa, los valores positivos grandes marcan zonas de sobrecompra.
  3. Requerir que la vela anterior perfore el valor actual de la media móvil (Low < MA para configuraciones alcistas y High > MA para las bajistas).
  4. Entrar cuando no hay posición abierta:
    • Largo: vela anterior alcista, mínimo por debajo de MA, Chaikin por debajo de -OpenLevel.
    • Corto: vela anterior bajista, máximo por encima de MA, Chaikin por encima de OpenLevel.
  5. Cuando existe una posición y aparece una configuración opuesta, el algoritmo revierte la posición neta (SellMarket / BuyMarket con volumen extra) para reflejar el comportamiento de cobertura de la versión MT5.
  6. Los stops y objetivos se emulan dentro de la estrategia usando máximos/mínimos de velas, porque StockSharp trabaja con posiciones netas en lugar de tickets individuales cubiertos.

Gestión de riesgos

  • Stop-loss y take-profit: distancias en pips convertidas a través del paso de precio del instrumento. Ambos pueden deshabilitarse con cero.
  • Break-even: una vez que el precio se mueve BreakevenPips a favor, el stop se mueve al precio de entrada.
  • Trailing: después de que el movimiento supera BreakevenPips + TrailingStopPips, el stop sigue al precio a la distancia de trailing.
  • Restablecimiento del estado de posición: cada vez que ocurre una salida, todos los niveles de precio internos se limpian para prepararse para el próximo trade.

Parámetros

Nombre Descripción
OpenLevel Magnitud del Chaikin requerida para abrir una nueva posición (por defecto 100).
CloseLevel Magnitud del Chaikin requerida para revertir una posición existente (por defecto 125).
StopLossPips Distancia del stop-loss en pips (por defecto 65).
TakeProfitPips Distancia del take-profit en pips (por defecto 100).
TrailingStopPips Distancia de trailing en pips (por defecto 30).
BreakevenPips Ganancia en pips antes de mover el stop a break-even (por defecto 15).
MaPeriod Longitud de la media móvil para el filtro de precio (por defecto 20).
MaShift Barras para desplazar la media móvil (por defecto 0).
MaType Tipo de media móvil (Simple, Exponential, Smoothed, Weighted).
ChaikinFastPeriod Longitud de suavizado rápido en el oscilador Chaikin (por defecto 3).
ChaikinSlowPeriod Longitud de suavizado lento en el oscilador Chaikin (por defecto 10).
ChaikinMaType Tipo de media móvil usada para el suavizado de Chaikin.
CandleType Serie de velas usada para cálculos.

Notas

  • Configurar la propiedad base Volume en StockSharp para controlar el tamaño de la operación.
  • Los pips se calculan usando el PriceStep del instrumento. Si el paso corresponde a cotizaciones de 3 o 5 decimales (p.ej., 0.00001), la estrategia lo multiplica por 10 para coincidir con el espaciado de pips de MetaTrader.
  • Debido a que StockSharp usa posiciones netas, las órdenes de cobertura del asesor experto MQL original se representan como reversiones inmediatas de la posición existente.
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>
/// Chaikin oscillator oversold/overbought strategy with optional reversal hedging and trailing management.
/// </summary>
public class PipsoverChaikinHedgeStrategy : Strategy
{
	private readonly StrategyParam<decimal> _openLevel;
	private readonly StrategyParam<decimal> _closeLevel;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _breakevenPips;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<MovingAverageTypeOptions> _maType;
	private readonly StrategyParam<int> _chaikinFastPeriod;
	private readonly StrategyParam<int> _chaikinSlowPeriod;
	private readonly StrategyParam<MovingAverageTypeOptions> _chaikinMaType;
	private readonly StrategyParam<DataType> _candleType;

	private AccumulationDistributionLine _adLine = null!;
	private IIndicator _priceMa = null!;
	private IIndicator _chaikinFast = null!;
	private IIndicator _chaikinSlow = null!;

	private readonly Queue<decimal> _maValues = new();

	private decimal _pipSize;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private decimal _prevOpen;
	private decimal _prevClose;
	private decimal _prevHigh;
	private decimal _prevLow;
	private bool _hasPrevCandle;
	private decimal _prevChaikin;
	private bool _hasPrevChaikin;

	/// <summary>
	/// Chaikin threshold for entries.
	/// </summary>
	public decimal OpenLevel
	{
		get => _openLevel.Value;
		set => _openLevel.Value = value;
	}

	/// <summary>
	/// Chaikin threshold for hedging reversals.
	/// </summary>
	public decimal CloseLevel
	{
		get => _closeLevel.Value;
		set => _closeLevel.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Breakeven activation distance in pips.
	/// </summary>
	public decimal BreakevenPips
	{
		get => _breakevenPips.Value;
		set => _breakevenPips.Value = value;
	}

	/// <summary>
	/// Moving average length.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Moving average shift in bars.
	/// </summary>
	public int MaShift
	{
		get => _maShift.Value;
		set => _maShift.Value = value;
	}

	/// <summary>
	/// Moving average type for price filter.
	/// </summary>
	public MovingAverageTypeOptions MaType
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	/// <summary>
	/// Fast Chaikin moving average length.
	/// </summary>
	public int ChaikinFastPeriod
	{
		get => _chaikinFastPeriod.Value;
		set => _chaikinFastPeriod.Value = value;
	}

	/// <summary>
	/// Slow Chaikin moving average length.
	/// </summary>
	public int ChaikinSlowPeriod
	{
		get => _chaikinSlowPeriod.Value;
		set => _chaikinSlowPeriod.Value = value;
	}

	/// <summary>
	/// Moving average type used in Chaikin oscillator.
	/// </summary>
	public MovingAverageTypeOptions ChaikinMaType
	{
		get => _chaikinMaType.Value;
		set => _chaikinMaType.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 <see cref="PipsoverChaikinHedgeStrategy"/> class.
	/// </summary>
	public PipsoverChaikinHedgeStrategy()
	{
		_openLevel = Param(nameof(OpenLevel), 0.01m)
		.SetGreaterThanZero()
		.SetDisplay("Open Level", "Chaikin level for entries", "Chaikin");

		_closeLevel = Param(nameof(CloseLevel), 0.02m)
		.SetGreaterThanZero()
		.SetDisplay("Close Level", "Chaikin level for hedging", "Chaikin");

		_stopLossPips = Param(nameof(StopLossPips), 65m)
		.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 100m)
		.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 30m)
		.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");

		_breakevenPips = Param(nameof(BreakevenPips), 15m)
		.SetDisplay("Breakeven (pips)", "Breakeven activation distance", "Risk");

		_maPeriod = Param(nameof(MaPeriod), 20)
		.SetGreaterThanZero()
		.SetDisplay("MA Period", "Price moving average length", "Trend");

		_maShift = Param(nameof(MaShift), 0)
		.SetDisplay("MA Shift", "Price moving average shift", "Trend");

		_maType = Param(nameof(MaType), MovingAverageTypeOptions.Simple)
		.SetDisplay("MA Type", "Price moving average type", "Trend");

		_chaikinFastPeriod = Param(nameof(ChaikinFastPeriod), 3)
		.SetGreaterThanZero()
		.SetDisplay("Chaikin Fast", "Fast Chaikin length", "Chaikin");

		_chaikinSlowPeriod = Param(nameof(ChaikinSlowPeriod), 10)
		.SetGreaterThanZero()
		.SetDisplay("Chaikin Slow", "Slow Chaikin length", "Chaikin");

		_chaikinMaType = Param(nameof(ChaikinMaType), MovingAverageTypeOptions.Exponential)
		.SetDisplay("Chaikin MA Type", "Chaikin moving average type", "Chaikin");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe for analysis", "Data");
	}

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

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

		_maValues.Clear();
		_pipSize = 0m;
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
		_prevOpen = 0m;
		_prevClose = 0m;
		_prevHigh = 0m;
		_prevLow = 0m;
		_prevChaikin = 0m;
		_hasPrevCandle = false;
		_hasPrevChaikin = false;
	}

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

		_pipSize = CalculatePipSize();

		_adLine = new AccumulationDistributionLine();
		_priceMa = CreateMovingAverage(MaType, MaPeriod);
		_chaikinFast = CreateMovingAverage(ChaikinMaType, ChaikinFastPeriod);
		_chaikinSlow = CreateMovingAverage(ChaikinMaType, ChaikinSlowPeriod);

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(_priceMa, _adLine, ProcessCandle)
		.Start();
	}

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

		var prevOpen = _prevOpen;
		var prevClose = _prevClose;
		var prevHigh = _prevHigh;
		var prevLow = _prevLow;
		var prevChaikin = _prevChaikin;
		var hasPrevCandle = _hasPrevCandle;
		var hasPrevChaikin = _hasPrevChaikin;

		var fastValue = _chaikinFast.Process(new DecimalIndicatorValue(_chaikinFast, adValue, candle.OpenTime) { IsFinal = true });
		var slowValue = _chaikinSlow.Process(new DecimalIndicatorValue(_chaikinSlow, adValue, candle.OpenTime) { IsFinal = true });

		if (!fastValue.IsFinal || !slowValue.IsFinal)
		{
			_prevChaikin = fastValue.ToDecimal() - slowValue.ToDecimal();
			_hasPrevChaikin = true;
			StorePreviousCandle(candle);
			return;
		}

		var chaikin = fastValue.ToDecimal() - slowValue.ToDecimal();
		var shiftedMa = UpdateShiftedMa(maValue);

		if (shiftedMa is null)
		{
			_prevChaikin = chaikin;
			_hasPrevChaikin = true;
			StorePreviousCandle(candle);
			return;
		}

		var hasPrevData = hasPrevCandle && hasPrevChaikin;
		var positionClosed = HandleStopsAndTargets(candle);
		var reversed = false;

		if (Position == 0m)
		{
			if (hasPrevData)
			{
				var bullishPrev = prevClose > prevOpen;
				var bearishPrev = prevClose < prevOpen;

				if (bullishPrev && prevLow < shiftedMa && prevChaikin < -OpenLevel)
				{
					BuyMarket(Volume);
					SetupLongTargets(candle.ClosePrice);
				}
				else if (bearishPrev && prevHigh > shiftedMa && prevChaikin > OpenLevel)
				{
					SellMarket(Volume);
					SetupShortTargets(candle.ClosePrice);
				}
			}
		}
		else if (!positionClosed)
		{
			if (hasPrevData)
			{
				var bearishPrev = prevClose < prevOpen;
				var bullishPrev = prevClose > prevOpen;

				if (Position > 0m && bearishPrev && prevHigh > shiftedMa && prevChaikin > CloseLevel)
				{
					var size = Math.Abs(Position) + Volume;
					SellMarket(size);
					SetupShortTargets(candle.ClosePrice);
					reversed = true;
				}
				else if (Position < 0m && bullishPrev && prevLow < shiftedMa && prevChaikin < -CloseLevel)
				{
					var size = Math.Abs(Position) + Volume;
					BuyMarket(size);
					SetupLongTargets(candle.ClosePrice);
					reversed = true;
				}
			}

			if (!reversed)
			UpdateTrailing(candle);
		}

		_prevChaikin = chaikin;
		_hasPrevChaikin = true;
		StorePreviousCandle(candle);
	}
	private decimal? UpdateShiftedMa(decimal maValue)
	{
		var shift = Math.Max(0, MaShift);
		_maValues.Enqueue(maValue);

		while (_maValues.Count > shift + 1)
		_maValues.Dequeue();

		var values = _maValues.ToArray();
		if (values.Length < shift + 1)
		return null;

		return values[0];
	}

	private void StorePreviousCandle(ICandleMessage candle)
	{
		_prevOpen = candle.OpenPrice;
		_prevClose = candle.ClosePrice;
		_prevHigh = candle.HighPrice;
		_prevLow = candle.LowPrice;
		_hasPrevCandle = true;
	}

	private bool HandleStopsAndTargets(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			if (_stopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}

			if (_takeProfitPrice is decimal take && candle.HighPrice >= take)
			{
				SellMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}
		}
		else if (Position < 0m)
		{
			if (_stopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}

			if (_takeProfitPrice is decimal take && candle.LowPrice <= take)
			{
				BuyMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}
		}

		return false;
	}

	private void SetupLongTargets(decimal price)
	{
		_entryPrice = price;

		if (StopLossPips > 0m)
		_stopPrice = price - StopLossPips * _pipSize;
		else
		_stopPrice = null;

		if (TakeProfitPips > 0m)
		_takeProfitPrice = price + TakeProfitPips * _pipSize;
		else
		_takeProfitPrice = null;
	}

	private void SetupShortTargets(decimal price)
	{
		_entryPrice = price;

		if (StopLossPips > 0m)
		_stopPrice = price + StopLossPips * _pipSize;
		else
		_stopPrice = null;

		if (TakeProfitPips > 0m)
		_takeProfitPrice = price - TakeProfitPips * _pipSize;
		else
		_takeProfitPrice = null;
	}

	private void UpdateTrailing(ICandleMessage candle)
	{
		if (_entryPrice is not decimal entry)
		return;

		var breakevenDist = BreakevenPips > 0m ? BreakevenPips * _pipSize : 0m;
		var trailingDist = TrailingStopPips > 0m ? TrailingStopPips * _pipSize : 0m;

		if (Position > 0m)
		{
			var move = candle.ClosePrice - entry;

			if (breakevenDist > 0m && move > breakevenDist)
			{
				if (_stopPrice is null || _stopPrice < entry)
				_stopPrice = entry;
			}

			if (trailingDist > 0m)
			{
				var activation = breakevenDist + trailingDist;
				if (move > activation)
				{
					var newStop = candle.ClosePrice - trailingDist;
					if (_stopPrice is null || newStop > _stopPrice)
					_stopPrice = newStop;
				}
			}
		}
		else if (Position < 0m)
		{
			var move = entry - candle.ClosePrice;

			if (breakevenDist > 0m && move > breakevenDist)
			{
				if (_stopPrice is null || _stopPrice > entry)
				_stopPrice = entry;
			}

			if (trailingDist > 0m)
			{
				var activation = breakevenDist + trailingDist;
				if (move > activation)
				{
					var newStop = candle.ClosePrice + trailingDist;
					if (_stopPrice is null || newStop < _stopPrice)
					_stopPrice = newStop;
				}
			}
		}
	}

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

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0.0001m;
		if (step <= 0m)
		step = 0.0001m;

		var tmp = step;
		var decimals = 0;

		while (tmp < 1m && decimals < 10)
		{
			tmp *= 10m;
			decimals++;
		}

		return decimals == 3 || decimals == 5 ? step * 10m : step;
	}

	private static IIndicator CreateMovingAverage(MovingAverageTypeOptions type, int length)
	{
		return type switch
		{
			MovingAverageTypeOptions.Simple => new SimpleMovingAverage { Length = length },
			MovingAverageTypeOptions.Exponential => new ExponentialMovingAverage { Length = length },
			MovingAverageTypeOptions.Smoothed => new SmoothedMovingAverage { Length = length },
			MovingAverageTypeOptions.Weighted => new WeightedMovingAverage { Length = length },
			_ => new SimpleMovingAverage { Length = length }
		};
	}

	/// <summary>
	/// Moving average options matching the MetaTrader configuration.
	/// </summary>
	public enum MovingAverageTypeOptions
	{
		Simple,
		Exponential,
		Smoothed,
		Weighted
	}
}