GitHub で見る

iCCI iMA 戦略

この戦略は MetaTrader のエキスパートアドバイザー「iCCI iMA」を複製します。Commodity Channel Index(CCI)のクロスオーバーを、CCI ストリームに直接適用された指数移動平均(EMA)に対してトレードします。独自の期間で計算されるセカンダリ CCI が、±100 バンド周辺の買われ過ぎ/売られ過ぎの反転を監視します。注文はロットでサイズが設定され、オプションで口座残高によってスケーリングされ、各トレードは pip で表現された設定可能なストップロスとテイクプロフィットレベルで保護されます。

仕組み

  • データソース – 設定可能なローソク足シリーズ(デフォルトで 1 分足ローソク足)が、ローソク足の典型的な価格 (high + low + close) / 3 を使用してすべてのインジケーター計算を供給します。
  • コアインジケーター – プライマリ CCI は CciPeriod の長さでモメンタムを測定します。その CCI の EMA(長さ MaPeriod)がオシレーターを平滑化し、シグナルラインとして機能します。セカンダリ CciClosePeriod CCI が閾値クロスオーバーを監視します。
  • エントリーロジック – 現在の CCI がその EMA を上回り、2 本の完成したローソク足前の値が EMA を下回っていた場合にロングポジションがオープンし、上向きクロスオーバーを示します。ショートポジションは CCI が下向きにクロスするときにこのロジックを反映します。アルゴリズムは、すべてのインジケーターが完全に形成され、2 つの履歴バーが利用可能になってから、MQL 実装の元のルックバックを再現するためにのみトレードします。
  • エグジットロジック – 既存のロングは、セカンダリ CCI が +100 を下回って戻るとき、または 2 バー前に EMA の上にあった後にプライマリ CCI が EMA を下回って下落するときにクローズします。ショートは、セカンダリ CCI が −100 を超えて上昇するとき、または同じ 2 バー確認の下で CCI が EMA の上に戻って上昇するときに出ます。保護ストップは完成した各ローソク足を監視します:ロングポジションは価格が entry − stopLossPips * pipSize まで下がるとクローズし、entry + takeProfitPips * pipSize で利益を取ります;ショートは entry + stopLossentry − takeProfit で対称レベルを使用します。pip サイズは有価証券の価格ステップから導出され、ティックサイズに 10 を掛けることで 3 桁または 5 桁の見積もりに適応し、MetaTrader の変換に一致します。
  • ポジションサイジング – ベースロットサイズ(LotSize)は、注文が取引所の制約を尊重するように商品の VolumeStepMinVolumeMaxVolume の値に対して検証されます。マネー管理が有効な場合、戦略は口座残高を DepositPerLot で割った整数係数(20 に制限)をロットサイズに掛け、各バーで更新し、元のエキスパートからの整数ステップスケーリングを再現します。

パラメーター

  • ローソク足タイプ – インジケーター計算に使用されるデータシリーズ。
  • CCI 期間 – クロスオーバーシグナルを駆動するプライマリ CCI の長さ。
  • CCI クローズ期間 – ±100 反転を監視するために使用されるセカンダリ CCI の長さ。
  • CCI EMA 期間 – プライマリ CCI 値を平滑化する EMA の期間。
  • ロットサイズ – スケーリング前のロット単位のベース取引ボリューム。
  • マネー管理を有効化 – 残高ベースのロットサイズスケーリングを切り替えます。
  • ロットあたり預金 – ロット乗数を 1 増やすために必要な残高増分(マネー管理が有効な場合のみアクティブ)。
  • ストップロス (pips) – pip 単位の保護ストップ距離;無効化するにはゼロに設定。
  • テイクプロフィット (pips) – pip 単位の利益目標距離;無効化するにはゼロに設定。

アルゴリズムは、2 バークロスオーバー比較がソース MQL ロジックと一致するように、取引を開始する前に 2 本の完全に完成したローソク足を必要とします。ストップロスとテイクプロフィットのチェックは、高値/安値の極値を使用して閉じたローソク足で評価され、StockSharp の高レベル API 内で MetaTrader のサーバー側保護注文を近似します。

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;
	}
}