Ver en GitHub

Estrategia CMO Dúplex

La estrategia es un puerto de StockSharp del experto MetaTrader 5 Exp_CMO_Duplex.mq5. Divide la lógica en dos patas independientes (larga y corta) que ambas reaccionan a los cruces de la línea cero del Oscilador de Momentum Chande (CMO). Cada pata puede consumir su propia serie de velas, período y desplazamiento de señal, lo que hace posible ejecutar configuraciones asimétricas en el mismo instrumento.

Cómo funciona

  • La estrategia se suscribe a uno o dos feeds de velas dependiendo de si las patas larga y corta usan el mismo DataType.
  • Cada pata posee su propia instancia del indicador CMO. El indicador se evalúa solo en velas terminadas.
  • La configuración SignalBar define cuántas velas completadas hacia atrás en el historial deben usarse para la lógica de cruce. Un valor de 0 significa «usar la barra cerrada más reciente», 1 usa la barra anterior, 2 usa la barra anterior a esa, y así sucesivamente.
  • Pata larga: cuando el valor CMO seleccionado cruza de por encima de cero a cero o por debajo, la estrategia entra (o cambia a) una posición larga si las entradas largas están permitidas. Las salidas largas se activan cuando el valor antiguo del CMO está por debajo de cero o cuando se tocan los niveles de stop loss / take profit.
  • Pata corta: refleja la lógica larga. Un cruce de por debajo de cero a cero o por encima abre (o cambia a) una posición corta y el signo opuesto del valor CMO o los stops configurados cierran la posición.
  • Los cambios de posición reutilizan Volume más cualquier exposición opuesta, por lo que una sola orden de mercado cierra la posición anterior y abre la nueva.
  • StartProtection() está habilitado en el inicio, por lo que los controles de riesgo integrados de StockSharp permanecen activos.

Parámetros

Parámetro Descripción
LongCandleType Tipo de vela usado por la pata larga.
LongCmoPeriod Período del indicador CMO en el lado largo.
LongSignalBar Número de barras cerradas entre el tiempo actual y la barra analizada para señales (0 = última barra cerrada).
EnableLongEntries Permite o bloquea abrir nuevas posiciones largas.
EnableLongExits Permite o bloquea cerrar posiciones largas en señales del oscilador.
LongStopLossPoints Distancia del stop-loss en pasos de precio para operaciones largas (0 deshabilita el stop).
LongTakeProfitPoints Distancia del take-profit en pasos de precio para operaciones largas (0 deshabilita el objetivo).
ShortCandleType Tipo de vela usado por la pata corta.
ShortCmoPeriod Período del indicador CMO en el lado corto.
ShortSignalBar Número de barras cerradas entre el tiempo actual y la barra analizada para señales cortas.
EnableShortEntries Permite o bloquea abrir nuevas posiciones cortas.
EnableShortExits Permite o bloquea cerrar posiciones cortas en señales del oscilador.
ShortStopLossPoints Distancia del stop-loss en pasos de precio para operaciones cortas (0 deshabilita el stop).
ShortTakeProfitPoints Distancia del take-profit en pasos de precio para operaciones cortas (0 deshabilita el objetivo).

La propiedad base Strategy.Volume controla el tamaño predeterminado de la orden. Cuando la estrategia necesita cambiar de dirección, envía una orden de mercado cuyo volumen es igual a Volume + |Position|, lo que cierra la exposición antigua y abre la nueva en una sola transacción.

Gestión de riesgos

  • Los niveles de stop-loss y take-profit se evalúan en cada vela terminada. Para posiciones largas el stop se coloca por debajo de la entrada y el objetivo por encima; para posiciones cortas los niveles se invierten.
  • Un stop o un objetivo activa una orden de mercado inmediata para cerrar la posición. La misma rutina de salida también se ejecuta cuando el valor del oscilador respectivo mantiene el signo incorrecto (por debajo de cero para largos, por encima de cero para cortos).
  • Establecer la distancia en cero deshabilita la protección correspondiente y deja la pata gestionada puramente por la lógica del oscilador.

Notas de uso

  • La estrategia funciona mejor en instrumentos donde el CMO tiende a revertir después de tocar la línea cero. Las entradas contrarias están deliberadamente retrasadas por el desplazamiento SignalBar para coincidir con el experto original.
  • Las patas larga y corta pueden compartir el mismo feed de velas u operar en diferentes marcos temporales. Si ambas usan el mismo DataType, la estrategia reutiliza una sola suscripción para mejor rendimiento.
  • Dado que la estrategia opera en velas completadas, se recomienda suministrar un flujo continuo de velas (por ejemplo, a través de un backtest histórico o un feed en tiempo real) para evitar señales perdidas.
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>
/// Two-sided strategy built around the Chande Momentum Oscillator zero-line crossings.
/// Long and short legs can use different candle types, periods and signal offsets.
/// </summary>
public class CmoDuplexStrategy : Strategy
{
	private readonly StrategyParam<DataType> _longCandleType;
	private readonly StrategyParam<int> _longCmoPeriod;
	private readonly StrategyParam<int> _longSignalBar;
	private readonly StrategyParam<bool> _enableLongEntries;
	private readonly StrategyParam<bool> _enableLongExits;
	private readonly StrategyParam<int> _longStopLossPoints;
	private readonly StrategyParam<int> _longTakeProfitPoints;

	private readonly StrategyParam<DataType> _shortCandleType;
	private readonly StrategyParam<int> _shortCmoPeriod;
	private readonly StrategyParam<int> _shortSignalBar;
	private readonly StrategyParam<bool> _enableShortEntries;
	private readonly StrategyParam<bool> _enableShortExits;
	private readonly StrategyParam<int> _shortStopLossPoints;
	private readonly StrategyParam<int> _shortTakeProfitPoints;

	private ChandeMomentumOscillator _longCmo;
	private ChandeMomentumOscillator _shortCmo;

	private readonly List<decimal> _longValues = new();
	private readonly List<decimal> _shortValues = new();

	private decimal? _entryPrice;

	public DataType LongCandleType
	{
		get => _longCandleType.Value;
		set => _longCandleType.Value = value;
	}

	public int LongCmoPeriod
	{
		get => _longCmoPeriod.Value;
		set => _longCmoPeriod.Value = value;
	}

	public int LongSignalBar
	{
		get => _longSignalBar.Value;
		set => _longSignalBar.Value = value;
	}

	public bool EnableLongEntries
	{
		get => _enableLongEntries.Value;
		set => _enableLongEntries.Value = value;
	}

	public bool EnableLongExits
	{
		get => _enableLongExits.Value;
		set => _enableLongExits.Value = value;
	}

	public int LongStopLossPoints
	{
		get => _longStopLossPoints.Value;
		set => _longStopLossPoints.Value = value;
	}

	public int LongTakeProfitPoints
	{
		get => _longTakeProfitPoints.Value;
		set => _longTakeProfitPoints.Value = value;
	}

	public DataType ShortCandleType
	{
		get => _shortCandleType.Value;
		set => _shortCandleType.Value = value;
	}

	public int ShortCmoPeriod
	{
		get => _shortCmoPeriod.Value;
		set => _shortCmoPeriod.Value = value;
	}

	public int ShortSignalBar
	{
		get => _shortSignalBar.Value;
		set => _shortSignalBar.Value = value;
	}

	public bool EnableShortEntries
	{
		get => _enableShortEntries.Value;
		set => _enableShortEntries.Value = value;
	}

	public bool EnableShortExits
	{
		get => _enableShortExits.Value;
		set => _enableShortExits.Value = value;
	}

	public int ShortStopLossPoints
	{
		get => _shortStopLossPoints.Value;
		set => _shortStopLossPoints.Value = value;
	}

	public int ShortTakeProfitPoints
	{
		get => _shortTakeProfitPoints.Value;
		set => _shortTakeProfitPoints.Value = value;
	}

	public CmoDuplexStrategy()
	{
		_longCandleType = Param(nameof(LongCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Long Candle Type", "Candle type for the long leg", "Long Leg");

		_longCmoPeriod = Param(nameof(LongCmoPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("Long CMO Period", "CMO period for the long leg", "Long Leg");

		_longSignalBar = Param(nameof(LongSignalBar), 1)
			.SetNotNegative()
			.SetDisplay("Long Signal Bar", "Offset in bars for long signals", "Long Leg");

		_enableLongEntries = Param(nameof(EnableLongEntries), true)
			.SetDisplay("Enable Long Entries", "Allow opening long trades", "Long Leg");

		_enableLongExits = Param(nameof(EnableLongExits), true)
			.SetDisplay("Enable Long Exits", "Allow closing long trades on signals", "Long Leg");

		_longStopLossPoints = Param(nameof(LongStopLossPoints), 1000)
			.SetNotNegative()
			.SetDisplay("Long Stop Loss", "Stop loss in price steps for longs", "Risk Management");

		_longTakeProfitPoints = Param(nameof(LongTakeProfitPoints), 2000)
			.SetNotNegative()
			.SetDisplay("Long Take Profit", "Take profit in price steps for longs", "Risk Management");

		_shortCandleType = Param(nameof(ShortCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Short Candle Type", "Candle type for the short leg", "Short Leg");

		_shortCmoPeriod = Param(nameof(ShortCmoPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("Short CMO Period", "CMO period for the short leg", "Short Leg");

		_shortSignalBar = Param(nameof(ShortSignalBar), 1)
			.SetNotNegative()
			.SetDisplay("Short Signal Bar", "Offset in bars for short signals", "Short Leg");

		_enableShortEntries = Param(nameof(EnableShortEntries), true)
			.SetDisplay("Enable Short Entries", "Allow opening short trades", "Short Leg");

		_enableShortExits = Param(nameof(EnableShortExits), true)
			.SetDisplay("Enable Short Exits", "Allow closing short trades on signals", "Short Leg");

		_shortStopLossPoints = Param(nameof(ShortStopLossPoints), 1000)
			.SetNotNegative()
			.SetDisplay("Short Stop Loss", "Stop loss in price steps for shorts", "Risk Management");

		_shortTakeProfitPoints = Param(nameof(ShortTakeProfitPoints), 2000)
			.SetNotNegative()
			.SetDisplay("Short Take Profit", "Take profit in price steps for shorts", "Risk Management");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, LongCandleType);

		if (!Equals(LongCandleType, ShortCandleType))
			yield return (Security, ShortCandleType);
	}

	protected override void OnReseted()
	{
		base.OnReseted();

		_longCmo = null;
		_shortCmo = null;
		_entryPrice = null;
		_longValues.Clear();
		_shortValues.Clear();
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_longCmo = new ChandeMomentumOscillator { Length = LongCmoPeriod };
		_shortCmo = new ChandeMomentumOscillator { Length = ShortCmoPeriod };

		var longSubscription = SubscribeCandles(LongCandleType);
		longSubscription.Bind(_longCmo, ProcessLongCandle);

		if (Equals(LongCandleType, ShortCandleType))
		{
			longSubscription.Bind(_shortCmo, ProcessShortCandle).Start();
		}
		else
		{
			longSubscription.Start();
			var shortSubscription = SubscribeCandles(ShortCandleType);
			shortSubscription.Bind(_shortCmo, ProcessShortCandle).Start();
		}

		// no fixed protection needed
	}

	private void ProcessLongCandle(ICandleMessage candle, decimal cmoValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_longCmo == null || !_longCmo.IsFormed)
			return;

		_longValues.Add(cmoValue);
		var shift = Math.Max(1, LongSignalBar);
		TrimBuffer(_longValues, shift + 3);

		if (_longValues.Count < shift + 1)
			return;

		var currentIndex = _longValues.Count - shift;
		var previousIndex = currentIndex - 1;
		if (previousIndex < 0)
			return;

		var current = _longValues[currentIndex];
		var previous = _longValues[previousIndex];

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (Position > 0 && _entryPrice is decimal entryPrice)
		{
			var step = Security?.PriceStep ?? 1m;
			var stopPrice = LongStopLossPoints > 0 ? entryPrice - LongStopLossPoints * step : (decimal?)null;
			var takePrice = LongTakeProfitPoints > 0 ? entryPrice + LongTakeProfitPoints * step : (decimal?)null;
			var exitBySignal = EnableLongExits && previous < 0m;

			if ((takePrice.HasValue && candle.HighPrice >= takePrice.Value) ||
				(stopPrice.HasValue && candle.LowPrice <= stopPrice.Value) ||
				exitBySignal)
			{
				SellMarket();
				_entryPrice = null;
			}
		}

		var crossDown = previous > 0m && current <= 0m;
		if (EnableLongEntries && crossDown && Position <= 0)
		{
			if (true)
			{
				BuyMarket();
				_entryPrice = candle.ClosePrice;
			}
		}
	}

	private void ProcessShortCandle(ICandleMessage candle, decimal cmoValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_shortCmo == null || !_shortCmo.IsFormed)
			return;

		_shortValues.Add(cmoValue);
		var shift = Math.Max(1, ShortSignalBar);
		TrimBuffer(_shortValues, shift + 3);

		if (_shortValues.Count < shift + 1)
			return;

		var currentIndex = _shortValues.Count - shift;
		var previousIndex = currentIndex - 1;
		if (previousIndex < 0)
			return;

		var current = _shortValues[currentIndex];
		var previous = _shortValues[previousIndex];

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (Position < 0 && _entryPrice is decimal entryPrice)
		{
			var step = Security?.PriceStep ?? 1m;
			var stopPrice = ShortStopLossPoints > 0 ? entryPrice + ShortStopLossPoints * step : (decimal?)null;
			var takePrice = ShortTakeProfitPoints > 0 ? entryPrice - ShortTakeProfitPoints * step : (decimal?)null;
			var exitBySignal = EnableShortExits && previous > 0m;

			if ((takePrice.HasValue && candle.LowPrice <= takePrice.Value) ||
				(stopPrice.HasValue && candle.HighPrice >= stopPrice.Value) ||
				exitBySignal)
			{
				BuyMarket();
				_entryPrice = null;
			}
		}

		var crossUp = previous < 0m && current >= 0m;
		if (EnableShortEntries && crossUp && Position >= 0)
		{
			if (true)
			{
				SellMarket();
				_entryPrice = candle.ClosePrice;
			}
		}
	}

	private static void TrimBuffer(List<decimal> values, int maxCount)
	{
		if (values.Count <= maxCount)
			return;

		values.RemoveRange(0, values.Count - maxCount);
	}
}