Открыть на GitHub

Стратегия CCI MA v1.5

Стратегия переносит советник MetaTrader «CCI_MA v1.5» на высокоуровневый API StockSharp. В оригинале сигналы возникают, когда Commodity Channel Index (CCI) пересекает простую скользящую среднюю, рассчитанную по самим значениям CCI, а дополнительный CCI контролирует возвраты из зон ±100. В версии для StockSharp сохранены порядок сигналов, опциональное мани-менеджмент правило и стопы/тейки в пунктах, при этом они реализованы через подписку на свечи и биндинг индикаторов.

Логика работы

  • Источник данных – Пользователь задаёт тип свечей (по умолчанию 15-минутные). Оба CCI считают значения по цене закрытия свечи, что повторяет использование PRICE_CLOSE в MetaTrader.
  • Основные индикаторы – Базовый CommodityChannelIndex с периодом CciPeriod оценивает импульс. Скользящая средняя SimpleMovingAverage длиной MaPeriod применяется к потоку CCI и образует сигнальную линию. Второй CCI (SignalCciPeriod) отслеживает выходы из зон перекупленности/перепроданности около ±100.
  • Вход в позицию – Длинная позиция открывается на свече, следующей за пересечением вверх: значение CCI на предыдущей свече должно быть выше своей SMA, а двумя свечами ранее – ниже. Короткий сигнал зеркален. При появлении нового сигнала существующая встречная позиция закрывается и переворачивается добавлением её объёма к новому ордеру, как и в MQL-версии.
  • Выход из позиции – Лонг закрывается, когда контрольный CCI падает с выше +100 до ниже +100 или когда основной CCI пересекает SMA вниз (анализируются две уже закрытые свечи). Шорты закрываются по обратным условиям. Стоп-лосс и тейк-профит имитируют пунктовую систему MetaTrader: стратегия вычисляет размер пункта из PriceStep инструмента (для трёх- и пятизначных котировок множитель 10) и сравнивает экстремумы свечи с уровнями вход ± расстояние на каждой завершённой свече.
  • Размер позиции – Параметр LotVolume задаёт базовый объём. Если включён UseMoneyManagement, объём умножается на целое число floor(balance / DepositPerLot) и ограничивается MaxMultiplier, что повторяет «лестницу» депозитов из оригинала. Перед отправкой объём приводится к VolumeStep, а также учитываются MinVolume и MaxVolume инструмента.

Параметры

  • Candle Type – Тип свечей для расчёта индикаторов.
  • CCI Period – Период основного осциллятора CCI.
  • Exit CCI Period – Период контрольного CCI для выходов по уровням ±100.
  • CCI MA Period – Период простой скользящей средней, применяемой к основному CCI.
  • Lot Volume – Базовый объём сделки до применения мани-менеджмента.
  • Enable Money Management – Включает масштабирование объёма по балансу.
  • Deposit Per Lot – Шаг баланса, который добавляет единицу к множителю объёма (используется только при активном мани-менеджменте).
  • Max Multiplier – Максимальный множитель для масштабирования объёма.
  • Stop Loss (pips) – Дистанция защитного стопа в пунктах (0 отключает).
  • Take Profit (pips) – Дистанция тейк-профита в пунктах (0 отключает).

Перед первой сделкой стратегия ждёт две полностью закрытые свечи, чтобы двухсвечные сравнения точно совпали с задержкой исполнения в MQL. Проверка стоп-лосса и тейк-профита происходит по закрытым свечам и их экстремумам, что приближает работу серверных защитных заявок MetaTrader в рамках высокоуровневого API StockSharp.

namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// Commodity Channel Index strategy converted from the MetaTrader "CCI_MA v1.5" expert advisor.
/// Uses a primary CCI with a manually computed SMA of CCI values as a signal line.
/// A secondary CCI provides overbought/oversold exit confirmation.
/// </summary>
public class CciMaV15Strategy : Strategy
{
	private readonly StrategyParam<int> _cciPeriod;
	private readonly StrategyParam<int> _signalCciPeriod;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<DataType> _candleType;

	private CommodityChannelIndex _cci;
	private CommodityChannelIndex _signalCci;

	private readonly List<decimal> _cciHistory = new();
	private decimal? _prevCciMa;
	private decimal? _prevCci;
	private decimal? _prevSignalCci;

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

	/// <summary>
	/// Secondary CCI period for exit signals.
	/// </summary>
	public int SignalCciPeriod
	{
		get => _signalCciPeriod.Value;
		set => _signalCciPeriod.Value = value;
	}

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

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

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

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

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public CciMaV15Strategy()
	{
		_cciPeriod = Param(nameof(CciPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("CCI Period", "Length of the primary CCI", "CCI")
			.SetOptimize(7, 35, 7);

		_signalCciPeriod = Param(nameof(SignalCciPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("Exit CCI Period", "Length of the secondary CCI", "CCI")
			.SetOptimize(7, 35, 7);

		_maPeriod = Param(nameof(MaPeriod), 9)
			.SetGreaterThanZero()
			.SetDisplay("CCI MA Period", "SMA length applied to the CCI", "CCI")
			.SetOptimize(3, 21, 3);

		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Protective stop distance in absolute points", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Profit target distance in absolute points", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Market data series", "General");

		Volume = 1;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_cci = null;
		_signalCci = null;
		_cciHistory.Clear();
		_prevCciMa = null;
		_prevCci = null;
		_prevSignalCci = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		_cci = new CommodityChannelIndex { Length = CciPeriod };
		_signalCci = new CommodityChannelIndex { Length = SignalCciPeriod };

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);

			var indArea = CreateChartArea();
			if (indArea != null)
			{
				DrawIndicator(indArea, _cci);
				DrawIndicator(indArea, _signalCci);
			}
		}

		var tp = TakeProfitPoints > 0 ? new Unit(TakeProfitPoints, UnitTypes.Absolute) : null;
		var sl = StopLossPoints > 0 ? new Unit(StopLossPoints, UnitTypes.Absolute) : null;
		if (tp != null || sl != null)
			StartProtection(tp, sl);

		base.OnStarted2(time);
	}

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

		// Maintain CCI history for manual SMA calculation
		_cciHistory.Add(cciValue);
		if (_cciHistory.Count > MaPeriod)
			_cciHistory.RemoveAt(0);

		// Compute SMA of CCI
		decimal? cciMa = null;
		if (_cciHistory.Count >= MaPeriod)
		{
			decimal sum = 0;
			for (int i = 0; i < _cciHistory.Count; i++)
				sum += _cciHistory[i];
			cciMa = sum / _cciHistory.Count;
		}

		if (cciMa == null || _prevCci == null || _prevCciMa == null || _prevSignalCci == null)
		{
			_prevCci = cciValue;
			_prevCciMa = cciMa;
			_prevSignalCci = signalCciValue;
			return;
		}

		// Exit logic: secondary CCI overbought/oversold reversal
		if (Position > 0 && _prevSignalCci > 100 && signalCciValue <= 100)
		{
			SellMarket(Position);
		}
		else if (Position < 0 && _prevSignalCci < -100 && signalCciValue >= -100)
		{
			BuyMarket(Math.Abs(Position));
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_prevCci = cciValue;
			_prevCciMa = cciMa;
			_prevSignalCci = signalCciValue;
			return;
		}

		// Entry: CCI crosses above its MA (buy) or below (sell)
		if (_prevCci < _prevCciMa && cciValue > cciMa.Value && Position <= 0)
		{
			if (Position < 0)
				BuyMarket(Math.Abs(Position));
			BuyMarket(Volume);
		}
		else if (_prevCci > _prevCciMa && cciValue < cciMa.Value && Position >= 0)
		{
			if (Position > 0)
				SellMarket(Position);
			SellMarket(Volume);
		}

		_prevCci = cciValue;
		_prevCciMa = cciMa;
		_prevSignalCci = signalCciValue;
	}
}