Auf GitHub ansehen

iCCI iMA Strategie

Diese Strategie repliziert den MetaTrader-Expertenberater «iCCI iMA». Sie handelt Commodity Channel Index (CCI)-Kreuzungen gegen einen exponentiellen gleitenden Durchschnitt (EMA), der direkt auf den CCI-Strom angewendet wird. Ein sekundärer CCI, mit seiner eigenen Periode berechnet, überwacht Überkauft-/Überverkauft-Umkehrungen um die ±100-Bänder. Orders werden in Lots dimensioniert, optional nach Kontostand skaliert, und jeder Trade wird durch konfigurierbare Stop-Loss- und Take-Profit-Niveaus in Pips geschützt.

Funktionsweise

  • Datenquelle – Eine konfigurierbare Kerzenserie (1-Minuten-Kerzen standardmäßig) speist alle Indikatorberechnungen mit dem typischen Kerzenpreis (high + low + close) / 3.
  • Kernindikatoren – Der primäre CCI misst den Momentum mit der CciPeriod-Länge. Ein EMA dieses CCI (Länge MaPeriod) glättet den Oszillator und fungiert als Signallinie. Der sekundäre CciClosePeriod-CCI überwacht Schwellenkreuzungen.
  • Einstiegslogik – Eine Long-Position öffnet sich, wenn der aktuelle CCI über seinem EMA liegt, während der Wert vor zwei abgeschlossenen Kerzen unter dem EMA lag, was auf einen Aufwärtskreuzung hinweist. Eine Short-Position spiegelt diese Logik wider, wenn der CCI nach unten kreuzt. Der Algorithmus handelt erst, wenn alle Indikatoren vollständig gebildet sind und zwei historische Balken verfügbar sind, um den ursprünglichen Look-Back der MQL-Implementierung zu reproduzieren.
  • Ausstiegslogik – Bestehende Longs schließen, wenn der sekundäre CCI wieder unter +100 fällt oder wenn der primäre CCI nach zwei Balken früher darüber die EMA unterschreitet. Shorts steigen aus, wenn der sekundäre CCI über −100 steigt oder wenn der CCI unter der gleichen Zwei-Balken-Bestätigung wieder über die EMA steigt. Schutzstops überwachen jede abgeschlossene Kerze: Long-Positionen schließen, wenn der Preis auf entry − stopLossPips * pipSize fällt und nehmen Gewinn bei entry + takeProfitPips * pipSize; Shorts verwenden die symmetrischen Niveaus mit entry + stopLoss und entry − takeProfit. Die Pip-Größe wird vom Preisschritt des Wertpapiers abgeleitet und passt sich durch Multiplikation der Tick-Größe mit 10 an 3- oder 5-stellige Kurse an, was der MetaTrader-Konvertierung entspricht.
  • Positionsgrößenbestimmung – Die Basis-Lot-Größe (LotSize) wird gegen die VolumeStep-, MinVolume- und MaxVolume-Werte des Instruments validiert, damit Orders Börsenbeschränkungen einhalten. Wenn Money-Management aktiviert ist, multipliziert die Strategie die Lot-Größe mit einem ganzzahligen Faktor gleich dem Kontostand dividiert durch DepositPerLot, auf 20 begrenzt, und auf jedem Balken aktualisiert, was das ganzzahlige Step-Scaling des ursprünglichen Experten reproduziert.

Parameter

  • Kerzentyp – Für Indikatorberechnungen verwendete Datenserie.
  • CCI-Periode – Länge des primären CCI, der die Kreuzungssignale antreibt.
  • CCI-Schlusskurs-Periode – Länge des sekundären CCI für die ±100-Umkehrungsüberwachung.
  • CCI-EMA-Periode – Periode des EMA, der die primären CCI-Werte glättet.
  • Lot-Größe – Basis-Handelsvolumen in Lots vor jeder Skalierung.
  • Money-Management aktivieren – Schaltet die saldenbasierte Skalierung der Lot-Größe um.
  • Einzahlung pro Lot – Saldeninkrement, das erforderlich ist, um den Lot-Multiplikator um eins zu erhöhen (nur aktiv, wenn Money-Management aktiviert ist).
  • Stop-Loss (Pips) – Schutzstop-Abstand in Pips; auf null setzen zum Deaktivieren.
  • Take-Profit (Pips) – Gewinnziel-Abstand in Pips; auf null setzen zum Deaktivieren.

Der Algorithmus erfordert zwei vollständig abgeschlossene Kerzen, bevor er mit dem Handel beginnt, damit die Zwei-Balken-Kreuzungsvergleiche mit der Quell-MQL-Logik übereinstimmen. Stop-Loss- und Take-Profit-Prüfungen werden auf geschlossenen Kerzen unter Verwendung ihrer Hoch/Tief-Extreme bewertet, was MetaTrader's serverseitige Schutzorders innerhalb der StockSharp High-Level-API approximiert.

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>
/// CCI and EMA crossover strategy converted from the MetaTrader iCCI iMA expert.
/// The strategy trades when the Commodity Channel Index crosses its exponential moving average.
/// </summary>
public class IcciImaStrategy : Strategy
{
	private readonly StrategyParam<int> _cciPeriod;
	private readonly StrategyParam<int> _cciClosePeriod;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<bool> _useMoneyManagement;
	private readonly StrategyParam<decimal> _depositPerLot;
	private readonly StrategyParam<decimal> _lotSize;
	private readonly StrategyParam<DataType> _candleType;

	private CommodityChannelIndex _cci = null!;
	private CommodityChannelIndex _cciClose = null!;
	private ExponentialMovingAverage _cciMa = null!;

	private decimal _pipSize;
	private decimal _lotMultiplier = 1m;
	private decimal? _entryPrice;
	private decimal? _prevCci;
	private decimal? _prev2Cci;
	private decimal? _prevCciClose;
	private decimal? _prev2CciClose;
	private decimal? _prevMa;
	private decimal? _prev2Ma;
	private int _historyCount;

	/// <summary>
	/// Constructor.
	/// </summary>
	public IcciImaStrategy()
	{
		_cciPeriod = Param(nameof(CciPeriod), 14)
		.SetGreaterThanZero()
		.SetDisplay("CCI Period", "Length of the main CCI indicator", "Indicators")
		
		.SetOptimize(5, 100, 1);

		_cciClosePeriod = Param(nameof(CciClosePeriod), 14)
		.SetGreaterThanZero()
		.SetDisplay("CCI Close Period", "Length of the CCI used for overbought and oversold exits", "Indicators")
		
		.SetOptimize(5, 100, 1);

		_maPeriod = Param(nameof(MaPeriod), 15)
		.SetGreaterThanZero()
		.SetDisplay("CCI EMA Period", "Length of the EMA applied to the CCI values", "Indicators")
		
		.SetOptimize(5, 100, 1);

		_stopLossPips = Param(nameof(StopLossPips), 50m)
		.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 40m)
		.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk");

		_useMoneyManagement = Param(nameof(UseMoneyManagement), false)
		.SetDisplay("Enable Money Management", "Scale position size by account balance", "Money Management");

		_depositPerLot = Param(nameof(DepositPerLot), 1000m)
		.SetGreaterThanZero()
		.SetDisplay("Deposit Per Lot", "Balance required to increase the lot multiplier", "Money Management");

		_lotSize = Param(nameof(LotSize), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Lot Size", "Base trading volume in lots", "Trading")
		
		.SetOptimize(0.01m, 1m, 0.01m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Data series used for calculations", "General");
	}

	/// <summary>
	/// Length of the primary CCI indicator.
	/// </summary>
	public int CciPeriod
	{
		get => _cciPeriod.Value;
		set => _cciPeriod.Value = value;
	}

	/// <summary>
	/// Length of the CCI used for exit signals around ±100.
	/// </summary>
	public int CciClosePeriod
	{
		get => _cciClosePeriod.Value;
		set => _cciClosePeriod.Value = value;
	}

	/// <summary>
	/// Exponential moving average period applied to the CCI values.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

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

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

	/// <summary>
	/// Enable adaptive money management.
	/// </summary>
	public bool UseMoneyManagement
	{
		get => _useMoneyManagement.Value;
		set => _useMoneyManagement.Value = value;
	}

	/// <summary>
	/// Deposit amount required to increase the lot multiplier by one.
	/// </summary>
	public decimal DepositPerLot
	{
		get => _depositPerLot.Value;
		set => _depositPerLot.Value = value;
	}

	/// <summary>
	/// Base trading volume in lots.
	/// </summary>
	public decimal LotSize
	{
		get => _lotSize.Value;
		set => _lotSize.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();
		ResetState();
	}

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

		ResetState();

		_cci = new CommodityChannelIndex
		{
			Length = CciPeriod
		};

		_cciClose = new CommodityChannelIndex
		{
			Length = CciClosePeriod
		};

		_cciMa = new EMA
		{
			Length = MaPeriod,
		};

		_pipSize = CalculatePipSize();

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

		// protection not needed
	}

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

		var maValue = _cciMa.Process(new DecimalIndicatorValue(_cciMa, cciValue, candle.OpenTime) { IsFinal = true }).ToDecimal();

		if (!_cci.IsFormed || !_cciClose.IsFormed || !_cciMa.IsFormed)
		{
			UpdateHistory(cciValue, cciCloseValue, maValue);
			return;
		}

		// Update the lot multiplier according to the current balance settings.
		UpdateMoneyManagement();

		if (_historyCount < 2)
		{
			UpdateHistory(cciValue, cciCloseValue, maValue);
			return;
		}

		// Check whether stop-loss or take-profit levels were touched on the latest candle.
		HandleStops(candle);

		// indicators formed check already done above

		var cciTwoBarsAgo = _prev2Cci ?? 0m;
		var maTwoBarsAgo = _prev2Ma ?? 0m;
		var cciCloseTwoBarsAgo = _prev2CciClose ?? 0m;

		// Determine exit conditions from the secondary CCI and the smoothed crossover.
		var shouldCloseLong = (cciCloseTwoBarsAgo > 100m && cciCloseValue <= 100m) || (cciValue < maValue && cciTwoBarsAgo >= maTwoBarsAgo);
		var shouldCloseShort = (cciCloseTwoBarsAgo < -100m && cciCloseValue >= -100m) || (cciValue > maValue && cciTwoBarsAgo <= maTwoBarsAgo);

		if (Position > 0 && shouldCloseLong)
		{
			SellMarket();
			_entryPrice = null;
		}
		else if (Position < 0 && shouldCloseShort)
		{
			BuyMarket();
			_entryPrice = null;
		}

		// Validate the requested lot size against security constraints.
		var volume = AdjustVolume(LotSize * _lotMultiplier);

		if (volume > 0m)
		{
			if (cciValue > maValue && cciTwoBarsAgo < maTwoBarsAgo && Position <= 0)
			{
				var totalVolume = volume + Math.Abs(Position);
				if (totalVolume > 0m)
				{
					BuyMarket();
					_entryPrice = candle.ClosePrice;
				}
			}
			else if (cciValue < maValue && cciTwoBarsAgo > maTwoBarsAgo && Position >= 0)
			{
				var totalVolume = volume + Math.Abs(Position);
				if (totalVolume > 0m)
				{
					SellMarket();
					_entryPrice = candle.ClosePrice;
				}
			}
		}

		if (Position == 0)
		_entryPrice = null;

		UpdateHistory(cciValue, cciCloseValue, maValue);
	}

	private void HandleStops(ICandleMessage candle)
	{
		if (_entryPrice == null)
		return;

		var priceStep = _pipSize > 0m ? _pipSize : Security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
		return;

		// Convert the configured pip distances into absolute price offsets.
		var stopLossDistance = StopLossPips > 0m ? StopLossPips * priceStep : 0m;
		var takeProfitDistance = TakeProfitPips > 0m ? TakeProfitPips * priceStep : 0m;

		if (Position > 0)
		{
			var entry = _entryPrice.Value;

			if (stopLossDistance > 0m && candle.LowPrice <= entry - stopLossDistance)
			{
				SellMarket();
				_entryPrice = null;
				return;
			}

			if (takeProfitDistance > 0m && candle.HighPrice >= entry + takeProfitDistance)
			{
				SellMarket();
				_entryPrice = null;
			}
		}
		else if (Position < 0)
		{
			var entry = _entryPrice.Value;
			var absPosition = Math.Abs(Position);

			if (stopLossDistance > 0m && candle.HighPrice >= entry + stopLossDistance)
			{
				BuyMarket();
				_entryPrice = null;
				return;
			}

			if (takeProfitDistance > 0m && candle.LowPrice <= entry - takeProfitDistance)
			{
				BuyMarket();
				_entryPrice = null;
			}
		}
	}

	private void UpdateMoneyManagement()
	{
		if (!UseMoneyManagement)
		{
			_lotMultiplier = 1m;
			return;
		}

		if (DepositPerLot <= 0m)
		return;

		var balance = Portfolio?.CurrentValue;
		if (balance == null || balance <= 0m)
		return;

		var ratio = (int)(balance.Value / DepositPerLot);
		if (ratio < 2)
		return;

		// Cap the multiplier at twenty lots, replicating the MQL expert behaviour.
		_lotMultiplier = Math.Min(20, ratio);
	}

	private void UpdateHistory(decimal cciValue, decimal cciCloseValue, decimal maValue)
	{
		// Shift cached values so the strategy can access readings from two completed candles ago.
		_prev2Cci = _prevCci;
		_prevCci = cciValue;

		_prev2CciClose = _prevCciClose;
		_prevCciClose = cciCloseValue;

		_prev2Ma = _prevMa;
		_prevMa = maValue;

		if (_historyCount < 2)
		_historyCount++;
	}

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

		var security = Security;
		if (security == null)
		return volume;

		var step = security.VolumeStep ?? 0m;
		if (step > 0m)
		{
			// Align the order size with the instrument volume step.
			var steps = Math.Floor(volume / step);
			volume = steps * step;
		}

		var minVolume = security.MinVolume ?? 0m;
		if (volume < minVolume)
		return 0m;

		var maxVolume = security.MaxVolume;
		if (maxVolume != null && volume > maxVolume.Value)
		volume = maxVolume.Value;

		return volume;
	}

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

		var bits = decimal.GetBits(priceStep);
		var scale = (bits[3] >> 16) & 0xFF;
		// Symbols with three or five digits require a tenfold pip multiplier.
		var multiplier = scale == 3 || scale == 5 ? 10m : 1m;

		return priceStep * multiplier;
	}

	private void ResetState()
	{
		// Restore cached values and multipliers before a new backtest/run.
		_pipSize = 0m;
		_lotMultiplier = 1m;
		_entryPrice = null;
		_prevCci = null;
		_prev2Cci = null;
		_prevCciClose = null;
		_prev2CciClose = null;
		_prevMa = null;
		_prev2Ma = null;
		_historyCount = 0;
	}
}