Стратегия 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, UnitTypes, Unit
from StockSharp.Algo.Indicators import CommodityChannelIndex
from StockSharp.Algo.Strategies import Strategy
class cci_ma_v15_strategy(Strategy):
"""
CCI MA v1.5 strategy. Uses primary CCI with SMA signal line and secondary CCI for exits.
"""
def __init__(self):
super(cci_ma_v15_strategy, self).__init__()
self._cci_period = self.Param("CciPeriod", 14).SetDisplay("CCI Period", "Length of the primary CCI", "CCI")
self._signal_cci_period = self.Param("SignalCciPeriod", 14).SetDisplay("Exit CCI Period", "Length of the secondary CCI", "CCI")
self._ma_period = self.Param("MaPeriod", 9).SetDisplay("CCI MA Period", "SMA length applied to the CCI", "CCI")
self._stop_loss_points = self.Param("StopLossPoints", 500.0).SetDisplay("Stop Loss", "Protective stop distance in absolute points", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 500.0).SetDisplay("Take Profit", "Profit target distance in absolute points", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Market data series", "General")
self._cci_history = []
self._prev_cci_ma = None
self._prev_cci = None
self._prev_signal_cci = None
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(cci_ma_v15_strategy, self).OnReseted()
self._cci_history = []
self._prev_cci_ma = None
self._prev_cci = None
self._prev_signal_cci = None
def OnStarted2(self, time):
super(cci_ma_v15_strategy, self).OnStarted2(time)
cci = CommodityChannelIndex()
cci.Length = self._cci_period.Value
signal_cci = CommodityChannelIndex()
signal_cci.Length = self._signal_cci_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(cci, signal_cci, self.on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
ind_area = self.CreateChartArea()
if ind_area is not None:
self.DrawIndicator(ind_area, cci)
self.DrawIndicator(ind_area, signal_cci)
tp = Unit(self._take_profit_points.Value, UnitTypes.Absolute) if self._take_profit_points.Value > 0 else None
sl = Unit(self._stop_loss_points.Value, UnitTypes.Absolute) if self._stop_loss_points.Value > 0 else None
if tp is not None or sl is not None:
self.StartProtection(tp, sl)
def on_process(self, candle, cci_value, signal_cci_value):
if candle.State != CandleStates.Finished:
return
self._cci_history.append(float(cci_value))
ma_period = self._ma_period.Value
if len(self._cci_history) > ma_period:
self._cci_history.pop(0)
cci_ma = None
if len(self._cci_history) >= ma_period:
cci_ma = sum(self._cci_history) / len(self._cci_history)
if cci_ma is None or self._prev_cci is None or self._prev_cci_ma is None or self._prev_signal_cci is None:
self._prev_cci = float(cci_value)
self._prev_cci_ma = cci_ma
self._prev_signal_cci = float(signal_cci_value)
return
# Exit logic
if self.Position > 0 and self._prev_signal_cci > 100 and float(signal_cci_value) <= 100:
self.SellMarket()
elif self.Position < 0 and self._prev_signal_cci < -100 and float(signal_cci_value) >= -100:
self.BuyMarket()
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_cci = float(cci_value)
self._prev_cci_ma = cci_ma
self._prev_signal_cci = float(signal_cci_value)
return
# Entry logic
if self._prev_cci < self._prev_cci_ma and float(cci_value) > cci_ma and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
elif self._prev_cci > self._prev_cci_ma and float(cci_value) < cci_ma and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._prev_cci = float(cci_value)
self._prev_cci_ma = cci_ma
self._prev_signal_cci = float(signal_cci_value)
def CreateClone(self):
return cci_ma_v15_strategy()