Ver no GitHub

Estratégia Simples Kloss

A Kloss Simple Strategy é uma conversão direta do MetaTrader 4 consultor especialista Kloss_.mq4. Ele reconstrói a ideia de negociação original usando StockSharp de alto nível API e mantém o conjunto de indicadores idêntico: uma média móvel exponencial (EMA) calculada sobre preços de fechamento ponderados, o índice de canal de commodities (CCI) e o oscilador Stochastic. Os sinais são gerados a partir da vela concluída anteriormente, espelhando a lógica de mudança de uma barra na versão MQL. O dimensionamento da posição pode basear-se num volume fixo de ordens ou numa percentagem de risco do valor da carteira, tal como as regras originais de cálculo do lote.

Ideia Central

  1. Monitore o contexto de impulso com limites CCI e Stochastic em torno de seus níveis neutros.
  2. Confirme os sinais de impulso com um EMA de curto prazo do preço de fechamento ponderado.
  3. Insira posições somente quando a vela anterior satisfizer todas as condições de sinal, evitando negociações prematuras com dados de mercado incompletos.
  4. Permitir múltiplas entradas na mesma direção até um limite configurável, emulando o parâmetro "MaxOrders" do script MT4.

Configuração do Indicador

  • EMA (MaPeriod): usa o fechamento ponderado (Close * 2 + High + Low) / 4 para corresponder a PRICE_WEIGHTED de MetaTrader. Atua como um filtro de tendências de curto prazo.
  • CCI (CciPeriod): Avalia desvios de momentum em relação ao preço médio. Limite ±CciLevel define entradas agressivas versus conservadoras.
  • Stochastic (StochasticKPeriod / DPeriod / Smooth): Usa a linha %K principal para detectar condições de sobrecompra ou sobrevenda em relação ao nível neutro 50. O desvio de 50 é controlado por StochasticLevel.

Todos os indicadores operam na série de velas primárias definida por CandleType. A estratégia atualiza os valores dos indicadores apenas nas velas finalizadas, garantindo backtesting estável e comportamento ao vivo.

Lógica de negociação

Configuração longa

  1. O fechamento da vela anterior está acima do valor anterior de EMA.
  2. O valor anterior de CCI está abaixo de -CciLevel, sinalizando impulso de sobrevenda.
  3. O valor anterior de Stochastic %K está abaixo de 50 - StochasticLevel, confirmando oscilação de sobrevenda.
  4. Quando as condições se mantêm, qualquer exposição curta é fechada e uma nova posição longa é aberta, desde que o número de ordens longas existentes seja inferior a MaxOrders.

Configuração curta

  1. O fechamento da vela anterior está abaixo do valor anterior de EMA.
  2. O valor anterior de CCI está acima de +CciLevel, sinalizando impulso de sobrecompra.
  3. O valor anterior de Stochastic %K está acima de 50 + StochasticLevel, confirmando a oscilação de sobrecompra.
  4. Quando as condições se mantêm, qualquer exposição longa é fechada e uma nova posição curta é aberta, sujeita ao limite MaxOrders.

Gerenciamento de saída

  • Stop Loss / Take Profit: Distâncias absolutas opcionais em pontos do instrumento. Se um dos valores for maior que zero, a proteção de posição integrada do StockSharp será ativada.
  • Sinal Oposto: Antes de abrir na direção oposta, a posição atual é fechada para imitar o consultor especialista original.

Dimensionamento de posições

  • OrderVolume: tamanho fixo padrão que replica o parâmetro Lots do MT4.
  • RiskPercentage: Quando maior que zero, a estratégia calcula o tamanho da negociação como uma porcentagem do valor do portfólio. Ele usa requisitos de margem de instrumento quando disponíveis, caso contrário, recorre ao dimensionamento baseado em preço, reproduzindo o comportamento Lots == 0 do código MQL.
  • MaxOrders: limita o volume cumulativo por direção, permitindo até MaxOrders * OrderVolume exposição.

Parâmetros

Parâmetro Descrição
OrderVolume Tamanho base do pedido usado quando RiskPercentage é zero.
MaPeriod Comprimento do EMA baseado nos preços de fechamento ponderados.
CciPeriod Número de barras usadas no cálculo CCI.
CciLevel Limite absoluto CCI para geração de sinal.
StochasticKPeriod Lookback para a linha Stochastic %K.
StochasticDPeriod Período médio móvel para a linha %D.
StochasticSmooth Suavização adicional aplicada a %K.
StochasticLevel Desvio de 50 usado para detecção de sobrecompra/sobrevenda.
MaxOrders Número máximo de entradas permitidas por direção.
StopLossPoints Distância de stop loss opcional em faixas de preço.
TakeProfitPoints Distância de lucro opcional em faixas de preço.
RiskPercentage Porcentagem do portfólio para dimensionamento dinâmico de posição.
CandleType Série de velas usada para todos os cálculos.

Notas práticas

  • Funciona melhor em dados intradiários, onde os osciladores de curto prazo reagem rapidamente às oscilações de preços.
  • O preço de fechamento ponderado mantém o EMA responsivo enquanto ainda incorpora a faixa máxima/mínima da vela.
  • Como toda decisão depende da vela anterior, a estratégia evita a repintura intra-barra e permanece determinística em testes históricos.
  • A gestão de risco deve estar alinhada com as especificações do contrato da corretora para que OrderVolume e MaxOrders correspondam aos tamanhos de negociação executáveis.
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;
	}
}