Ver en GitHub

Estrategia simple de Kloss

La Estrategia Simple de Kloss es una conversión directa del MetaTrader 4 asesores expertos Kloss_.mq4. Reconstruye la idea comercial original utilizando el nivel alto API de StockSharp y mantiene el conjunto de indicadores idéntico: un promedio móvil exponencial (EMA) calculado sobre los precios de cierre ponderados, el índice del canal de productos básicos (CCI) y el oscilador Stochastic. Las señales se generan a partir de la vela completada anteriormente, reflejando la lógica de cambio de una barra en la versión MQL. El tamaño de la posición puede depender de un volumen de orden fijo o de un porcentaje de riesgo del valor de la cartera, al igual que las reglas de cálculo de lotes originales.

Idea central

  1. Supervise el contexto del impulso con umbrales CCI y Stochastic alrededor de sus niveles neutrales.
  2. Confirme las señales de impulso con un EMA a corto plazo del precio de cierre ponderado.
  3. Ingrese posiciones solo cuando la vela anterior cumpla todas las condiciones de la señal, evitando operaciones prematuras con datos de mercado incompletos.
  4. Permita múltiples entradas en la misma dirección hasta un límite configurable, emulando el parámetro "MaxOrders" del script MT4.

Configuración del indicador

  • EMA (MaPeriod): utiliza el cierre ponderado (Close * 2 + High + Low) / 4 para hacer coincidir PRICE_WEIGHTED de MetaTrader. Actúa como un filtro de tendencias a corto plazo.
  • CCI (CciPeriod): Evalúa las desviaciones del impulso del precio medio. El umbral ±CciLevel define entradas agresivas versus conservadoras.
  • Stochastic (StochasticKPeriod / DPeriod / Smooth): Utiliza la línea principal %K para detectar condiciones de sobrecompra o sobreventa en relación con el nivel neutral 50. La desviación de 50 está controlada por StochasticLevel.

Todos los indicadores operan en la serie de velas primaria definida por CandleType. La estrategia actualiza los valores del indicador solo en las velas terminadas, lo que garantiza un backtesting estable y un comportamiento en vivo.

Lógica de trading

Configuración larga

  1. El cierre de la vela anterior está por encima del valor EMA anterior.
  2. El valor anterior de CCI está por debajo de -CciLevel, lo que indica un impulso de sobreventa.
  3. El valor %K anterior de Stochastic está por debajo de 50 - StochasticLevel, lo que confirma la oscilación de sobreventa.
  4. Cuando las condiciones se mantienen, cualquier exposición corta se cierra y se abre una nueva posición larga, siempre que el número de órdenes largas existentes sea inferior a MaxOrders.

Configuración corta

  1. El cierre de la vela anterior está por debajo del valor EMA anterior.
  2. El valor anterior de CCI está por encima de +CciLevel, lo que indica un impulso de sobrecompra.
  3. El valor %K anterior de Stochastic está por encima de 50 + StochasticLevel, lo que confirma la oscilación de sobrecompra.
  4. Cuando las condiciones se mantienen, cualquier exposición larga se cierra y se abre una nueva posición corta, sujeta al límite MaxOrders.

Gestión de salidas

  • Stop Loss / Take Profit: Distancias absolutas opcionales en puntos del instrumento. Si cualquiera de los valores es mayor que cero, se activa la protección de posición incorporada de StockSharp.
  • Señal opuesta: antes de abrir en la dirección opuesta, la posición actual se cierra para imitar al asesor experto original.

Dimensionamiento de posiciones

  • OrderVolume: tamaño fijo predeterminado que replica el parámetro Lots de MT4.
  • RiskPercentage: cuando es mayor que cero, la estrategia calcula el tamaño de la operación como un porcentaje del valor de la cartera. Utiliza requisitos de margen del instrumento cuando están disponibles; de lo contrario, recurre al tamaño basado en el precio, reproduciendo el comportamiento Lots == 0 del código MQL.
  • MaxOrders: limita el volumen acumulado por dirección permitiendo hasta MaxOrders * OrderVolume exposición.

Parámetros

Parámetro Descripción
OrderVolume Tamaño de pedido base utilizado cuando RiskPercentage es cero.
MaPeriod La duración del EMA se basa en precios de cierre ponderados.
CciPeriod Número de barras utilizadas en el cálculo CCI.
CciLevel Umbral absoluto CCI para la generación de señal.
StochasticKPeriod Búsqueda retrospectiva de la línea Stochastic %K.
StochasticDPeriod Período de media móvil para la línea %D.
StochasticSmooth Suavizado adicional aplicado a %K.
StochasticLevel Desviación de 50 utilizada para la detección de sobrecompra/sobreventa.
MaxOrders Número máximo de entradas permitidas por dirección.
StopLossPoints Distancia de stop loss opcional en puntos de precio.
TakeProfitPoints Distancia de toma de ganancias opcional en puntos de precio.
RiskPercentage Porcentaje de cartera para dimensionamiento dinámico de posiciones.
CandleType Serie de velas utilizada para todos los cálculos.

Notas prácticas

  • Funciona mejor con datos intradiarios donde los osciladores de corto plazo reaccionan rápidamente a las oscilaciones de precios.
  • El precio de cierre ponderado mantiene la capacidad de respuesta del EMA y al mismo tiempo incorpora el rango alto/bajo de la vela.
  • Debido a que cada decisión se basa en la vela anterior, la estrategia evita el repintado dentro de la barra y permanece determinista en las pruebas históricas.
  • La gestión de riesgos debe estar alineada con las especificaciones del contrato del corredor para que OrderVolume y MaxOrders correspondan a tamaños de operaciones ejecutables.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy based on EMA, CCI, and Stochastic oscillator signals.
/// Converts the original Kloss expert advisor from MetaTrader 4 to StockSharp.
/// </summary>
public class KlossSimpleStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _cciPeriod;
	private readonly StrategyParam<decimal> _cciLevel;
	private readonly StrategyParam<int> _stochasticKPeriod;
	private readonly StrategyParam<int> _stochasticDPeriod;
	private readonly StrategyParam<int> _stochasticSmooth;
	private readonly StrategyParam<decimal> _stochasticLevel;
	private readonly StrategyParam<int> _maxOrders;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _riskPercentage;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _ema;
	private CommodityChannelIndex _cci;
	private StochasticOscillator _stochastic;

	private decimal? _previousCci;

	/// <summary>
	/// Initializes a new instance of the <see cref="KlossSimpleStrategy"/> class.
	/// </summary>
	public KlossSimpleStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Base order volume", "Trading");

		_maPeriod = Param(nameof(MaPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("EMA Period", "Length of the exponential moving average", "Indicators")
			
			.SetOptimize(3, 20, 1);

		_cciPeriod = Param(nameof(CciPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("CCI Period", "Length of the commodity channel index", "Indicators")
			
			.SetOptimize(5, 30, 5);

		_cciLevel = Param(nameof(CciLevel), 200m)
			.SetGreaterThanZero()
			.SetDisplay("CCI Level", "Distance from zero to trigger signals", "Indicators")

			.SetOptimize(50m, 200m, 10m);

		_stochasticKPeriod = Param(nameof(StochasticKPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic %K", "Period of the %K line", "Indicators")
			
			.SetOptimize(3, 20, 1);

		_stochasticDPeriod = Param(nameof(StochasticDPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic %D", "Period of the %D line", "Indicators")

			.SetOptimize(1, 10, 1);

		_stochasticSmooth = Param(nameof(StochasticSmooth), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Smooth", "Smoothing factor for %K", "Indicators")
			
			.SetOptimize(1, 10, 1);

		_stochasticLevel = Param(nameof(StochasticLevel), 30m)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Level", "Distance from 50 to trigger signals", "Indicators")

			.SetOptimize(10m, 40m, 5m);

		_maxOrders = Param(nameof(MaxOrders), 1)
			.SetNotNegative()
			.SetDisplay("Max Orders", "Maximum number of positions per direction", "Trading");

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

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

		_riskPercentage = Param(nameof(RiskPercentage), 10m)
			.SetNotNegative()
			.SetDisplay("Risk %", "Portfolio percentage for dynamic position sizing", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle series for calculations", "General");
	}

	/// <summary>Base order volume for new entries.</summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>EMA length used to filter price action.</summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>CCI period for momentum detection.</summary>
	public int CciPeriod
	{
		get => _cciPeriod.Value;
		set => _cciPeriod.Value = value;
	}

	/// <summary>Absolute level that CCI must exceed to signal an entry.</summary>
	public decimal CciLevel
	{
		get => _cciLevel.Value;
		set => _cciLevel.Value = value;
	}

	/// <summary>Stochastic %K period.</summary>
	public int StochasticKPeriod
	{
		get => _stochasticKPeriod.Value;
		set => _stochasticKPeriod.Value = value;
	}

	/// <summary>Stochastic %D period.</summary>
	public int StochasticDPeriod
	{
		get => _stochasticDPeriod.Value;
		set => _stochasticDPeriod.Value = value;
	}

	/// <summary>Smoothing applied to the %K line.</summary>
	public int StochasticSmooth
	{
		get => _stochasticSmooth.Value;
		set => _stochasticSmooth.Value = value;
	}

	/// <summary>Offset around 50 used for stochastic thresholds.</summary>
	public decimal StochasticLevel
	{
		get => _stochasticLevel.Value;
		set => _stochasticLevel.Value = value;
	}

	/// <summary>Maximum number of simultaneous entries per direction.</summary>
	public int MaxOrders
	{
		get => _maxOrders.Value;
		set => _maxOrders.Value = value;
	}

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

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

	/// <summary>Portfolio percentage used to size positions dynamically.</summary>
	public decimal RiskPercentage
	{
		get => _riskPercentage.Value;
		set => _riskPercentage.Value = value;
	}

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

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

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

		_previousCci = null;
	}

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

		_ema = new EMA { Length = MaPeriod };
		_cci = new CommodityChannelIndex { Length = CciPeriod };
		_stochastic = new StochasticOscillator();
		_stochastic.K.Length = StochasticKPeriod;
		_stochastic.D.Length = StochasticDPeriod;

		Volume = OrderVolume;

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

		Unit stopLossUnit = null;
		Unit takeProfitUnit = null;
		var priceStep = Security?.PriceStep ?? 0m;

		if (StopLossPoints > 0m && priceStep > 0m)
		{
			stopLossUnit = new Unit(StopLossPoints * priceStep, UnitTypes.Absolute);
		}

		if (TakeProfitPoints > 0m && priceStep > 0m)
		{
			takeProfitUnit = new Unit(TakeProfitPoints * priceStep, UnitTypes.Absolute);
		}

		if (stopLossUnit != null || takeProfitUnit != null)
		{
			StartProtection(stopLoss: stopLossUnit, takeProfit: takeProfitUnit);
		}

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

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

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var maResult = _ema.Process(candle).ToNullableDecimal();
		var cciResult = _cci.Process(candle).ToNullableDecimal();
		var stochasticValue = (StochasticOscillatorValue)_stochastic.Process(candle);

		if (maResult == null || cciResult == null)
			return;

		if (stochasticValue.K is not decimal stochasticK)
			return;

		if (!_ema.IsFormed || !_cci.IsFormed || !_stochastic.IsFormed)
			return;

		var maValue = maResult.Value;
		var cciValue = cciResult.Value;

		var lowerStochastic = 50m - StochasticLevel;
		var upperStochastic = 50m + StochasticLevel;

		// Buy signal: CCI crosses up through -level from oversold territory
		var cciBuyXover = _previousCci != null && _previousCci.Value < -CciLevel && cciValue >= -CciLevel;
		// Sell signal: CCI crosses down through +level from overbought territory
		var cciSellXover = _previousCci != null && _previousCci.Value > CciLevel && cciValue <= CciLevel;

		if (cciBuyXover && stochasticK < lowerStochastic)
		{
			CloseShortPositions();
			TryEnterLong(candle);
		}
		else if (cciSellXover && stochasticK > upperStochastic)
		{
			CloseLongPositions();
			TryEnterShort(candle);
		}

		_previousCci = cciValue;
	}

	private void CloseLongPositions()
	{
		var longVolume = Position > 0m ? Position : 0m;
		if (longVolume <= 0m)
			return;

		// Close existing long volume before reversing into short trades.
		SellMarket(longVolume);
	}

	private void CloseShortPositions()
	{
		var shortVolume = Position < 0m ? Position.Abs() : 0m;
		if (shortVolume <= 0m)
			return;

		// Close existing short volume before opening new long trades.
		BuyMarket(shortVolume);
	}

	private void TryEnterLong(ICandleMessage candle)
	{
		var volume = CalculateOrderVolume(candle.ClosePrice);
		if (volume <= 0m)
			return;

		var currentLongVolume = Position > 0m ? Position : 0m;

		if (MaxOrders > 0)
		{
			var maxVolume = volume * MaxOrders;
			if (currentLongVolume >= maxVolume)
				return;

			var additionalVolume = volume.Min(maxVolume - currentLongVolume);
			if (additionalVolume <= 0m)
				return;

			// Add new long exposure without exceeding MaxOrders limit.
			BuyMarket(additionalVolume);
		}
		else
		{
			BuyMarket(volume);
		}
	}

	private void TryEnterShort(ICandleMessage candle)
	{
		var volume = CalculateOrderVolume(candle.ClosePrice);
		if (volume <= 0m)
			return;

		var currentShortVolume = Position < 0m ? Position.Abs() : 0m;

		if (MaxOrders > 0)
		{
			var maxVolume = volume * MaxOrders;
			if (currentShortVolume >= maxVolume)
				return;

			var additionalVolume = volume.Min(maxVolume - currentShortVolume);
			if (additionalVolume <= 0m)
				return;

			// Add new short exposure without exceeding MaxOrders limit.
			SellMarket(additionalVolume);
		}
		else
		{
			SellMarket(volume);
		}
	}

	private decimal CalculateOrderVolume(decimal referencePrice)
	{
		var volume = OrderVolume;

		if (RiskPercentage > 0m)
		{
			var portfolioValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
			var riskCapital = portfolioValue * RiskPercentage / 100m;

			if (riskCapital > 0m)
			{
				var margin = GetSecurityValue<decimal?>(Level1Fields.MarginBuy) ?? GetSecurityValue<decimal?>(Level1Fields.MarginSell) ?? 0m;

				if (margin > 0m)
				{
					volume = riskCapital / margin;
				}
				else if (referencePrice > 0m)
				{
					volume = riskCapital / referencePrice;
				}
			}
		}

		volume = RoundVolume(volume);

		var minVolume = Security?.MinVolume;
		if (minVolume != null && minVolume.Value > 0m && volume < minVolume.Value)
		{
			volume = minVolume.Value;
		}

		var maxVolume = Security?.MaxVolume;
		if (maxVolume != null && maxVolume.Value > 0m && volume > maxVolume.Value)
		{
			volume = maxVolume.Value;
		}

		return volume;
	}

	private decimal RoundVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var step = Security?.VolumeStep ?? 0m;
		if (step <= 0m)
			return volume;

		var steps = Math.Floor(volume / step);
		var rounded = steps * step;

		if (rounded <= 0m)
			rounded = step;

		return rounded;
	}
}