Ma2Cci Ema Strategy
Dual exponential moving average crossover strategy confirmed by a Commodity Channel Index (CCI) zero-line break. Position size and stop placement are derived from Average True Range (ATR) volatility and a configurable risk percentage.
Details
- Data: Time-based candles (default 1 hour) supplied by the selected
Candle Typeparameter. - Entry: Go long when the fast EMA crosses above the slow EMA and CCI crosses above zero on the same bar; go short on the opposite crossover with CCI breaking below zero.
- Exit: Close longs when the fast EMA crosses back below the slow EMA or price touches the fixed stop; close shorts when the fast EMA crosses above the slow EMA or price hits the short stop.
- Risk: Stop distance equals the greater of ATR (length
AtrPeriod) orMinStopPointsmultiplied by the instrument price step. Trade size is the portfolio value timesRiskPercent, divided by that stop distance. - Instruments: Trend-following forex or index symbols that support hedging in the original MetaTrader version; also applicable to other assets with clear momentum swings.
- Environment: Designed for continuous session markets where EMA/CCI signals align with ATR-based risk controls.
Parameters
CandleType– Timeframe and data type used for calculations and order flow.FastMaPeriod– Period of the fast EMA (default 10).SlowMaPeriod– Period of the slow EMA (default 37).CciPeriod– Lookback of the CCI oscillator confirming momentum (default 39).AtrPeriod– ATR length used to estimate current volatility for stop placement (default 3).RiskPercent– Fraction of current portfolio equity risked per trade (default 2%).MinStopPoints– Minimum stop distance expressed in price steps to emulate the MetaTrader pip filter (default 15).
Notes
- Works best when run on liquid pairs and indices where EMA/CCI crossings are reliable; thin markets can trigger premature exits.
- Because stops are recalculated only on entry, the strategy keeps the risk profile stable and mirrors the fixed stop-loss logic from the original MQL expert.
- Portfolio valuation must be provided by the connected account for the position sizing to operate; otherwise the engine falls back to the strategy
Volumeor instrument minimum volume.
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>
/// EMA crossover with CCI confirmation and ATR based stop distance.
/// </summary>
public class Ma2CciEmaStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _fastMaPeriod;
private readonly StrategyParam<int> _slowMaPeriod;
private readonly StrategyParam<int> _cciPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<int> _minStopPoints;
private ExponentialMovingAverage _fastMa = null!;
private ExponentialMovingAverage _slowMa = null!;
private CommodityChannelIndex _cci = null!;
private AverageTrueRange _atr = null!;
private decimal _previousFast;
private decimal _previousSlow;
private decimal _previousCci;
private bool _hasPreviousValues;
private decimal? _stopPrice;
/// <summary>
/// Candle type used to receive market data.
/// </summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
/// <summary>
/// Fast EMA period.
/// </summary>
public int FastMaPeriod { get => _fastMaPeriod.Value; set => _fastMaPeriod.Value = value; }
/// <summary>
/// Slow EMA period.
/// </summary>
public int SlowMaPeriod { get => _slowMaPeriod.Value; set => _slowMaPeriod.Value = value; }
/// <summary>
/// CCI calculation period.
/// </summary>
public int CciPeriod { get => _cciPeriod.Value; set => _cciPeriod.Value = value; }
/// <summary>
/// ATR period for volatility based stops.
/// </summary>
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
/// <summary>
/// Percentage of portfolio equity risked per trade.
/// </summary>
public decimal RiskPercent { get => _riskPercent.Value; set => _riskPercent.Value = value; }
/// <summary>
/// Minimum stop distance expressed in price steps.
/// </summary>
public int MinStopPoints { get => _minStopPoints.Value; set => _minStopPoints.Value = value; }
/// <summary>
/// Initializes a new instance of the <see cref="Ma2CciEmaStrategy"/> class.
/// </summary>
public Ma2CciEmaStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles used for calculations", "General");
_fastMaPeriod = Param(nameof(FastMaPeriod), 10)
.SetGreaterThanZero()
.SetDisplay("Fast EMA", "Fast EMA period", "Indicators");
_slowMaPeriod = Param(nameof(SlowMaPeriod), 37)
.SetGreaterThanZero()
.SetDisplay("Slow EMA", "Slow EMA period", "Indicators");
_cciPeriod = Param(nameof(CciPeriod), 39)
.SetGreaterThanZero()
.SetDisplay("CCI Period", "CCI length", "Indicators");
_atrPeriod = Param(nameof(AtrPeriod), 3)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "ATR length for stop calculation", "Risk Management");
_riskPercent = Param(nameof(RiskPercent), 2m)
.SetDisplay("Risk %", "Portfolio percentage risked per entry", "Risk Management");
_minStopPoints = Param(nameof(MinStopPoints), 15)
.SetGreaterThanZero()
.SetDisplay("Min Stop Points", "Minimum stop distance in price steps", "Risk Management");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousFast = 0m;
_previousSlow = 0m;
_previousCci = 0m;
_hasPreviousValues = false;
_stopPrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fastMa = new EMA { Length = FastMaPeriod };
_slowMa = new EMA { Length = SlowMaPeriod };
_cci = new CommodityChannelIndex { Length = CciPeriod };
_atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_fastMa, _slowMa, _cci, _atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _fastMa);
DrawIndicator(area, _slowMa);
DrawIndicator(area, _cci);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue, decimal cciValue, decimal atrValue)
{
// Process only finished candles to avoid intrabar noise.
if (candle.State != CandleStates.Finished)
return;
// Wait until all indicators are fully initialized before trading.
if (!_fastMa.IsFormed || !_slowMa.IsFormed || !_cci.IsFormed || !_atr.IsFormed)
return;
// removed IFOAAT for backtesting
if (!_hasPreviousValues)
{
_previousFast = fastValue;
_previousSlow = slowValue;
_previousCci = cciValue;
_hasPreviousValues = true;
return;
}
var fastCrossUp = _previousFast <= _previousSlow && fastValue > slowValue;
var fastCrossDown = _previousFast >= _previousSlow && fastValue < slowValue;
var cciCrossUp = _previousCci <= 0m && cciValue > 0m;
var cciCrossDown = _previousCci >= 0m && cciValue < 0m;
var stopDistance = Math.Max(atrValue, GetMinStopDistance());
if (Position != 0m)
{
var exitTriggered = false;
if (Position > 0m)
{
// Close long positions on stop hit or bearish crossover.
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
SellMarket();
exitTriggered = true;
}
else if (fastCrossDown)
{
SellMarket();
exitTriggered = true;
}
}
else if (Position < 0m)
{
// Close short positions on stop hit or bullish crossover.
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
BuyMarket();
exitTriggered = true;
}
else if (fastCrossUp)
{
BuyMarket();
exitTriggered = true;
}
}
if (exitTriggered)
{
_stopPrice = null;
_previousFast = fastValue;
_previousSlow = slowValue;
_previousCci = cciValue;
return;
}
}
else
{
// Enter long when EMA and CCI confirm bullish momentum.
if (fastCrossUp && cciCrossUp)
{
var volume = CalculateVolume(stopDistance);
if (volume > 0m)
{
BuyMarket();
_stopPrice = NormalizePrice(candle.ClosePrice - stopDistance);
}
}
// Enter short when EMA and CCI confirm bearish momentum.
else if (fastCrossDown && cciCrossDown)
{
var volume = CalculateVolume(stopDistance);
if (volume > 0m)
{
SellMarket();
_stopPrice = NormalizePrice(candle.ClosePrice + stopDistance);
}
}
}
_previousFast = fastValue;
_previousSlow = slowValue;
_previousCci = cciValue;
}
private decimal CalculateVolume(decimal stopDistance)
{
if (stopDistance <= 0m)
return 0m;
var equity = Portfolio?.CurrentValue ?? 0m;
var riskAmount = equity * (RiskPercent / 100m);
if (riskAmount <= 0m)
return NormalizeVolume(GetBaseVolume());
var rawVolume = riskAmount / stopDistance;
if (rawVolume <= 0m)
return NormalizeVolume(GetBaseVolume());
return NormalizeVolume(rawVolume);
}
private decimal GetBaseVolume()
{
var volume = Volume;
if (volume > 0m)
return volume;
var step = Security?.VolumeStep ?? 1m;
var min = Security?.MinVolume ?? step;
return min > 0m ? min : step;
}
private decimal NormalizeVolume(decimal volume)
{
var step = Security?.VolumeStep ?? 1m;
if (step <= 0m)
return Math.Max(volume, 0m);
var normalized = Math.Round(volume / step, MidpointRounding.AwayFromZero) * step;
var min = Security?.MinVolume ?? step;
if (normalized < min)
normalized = min;
var max = Security?.MaxVolume;
if (max.HasValue && max.Value > 0m && normalized > max.Value)
normalized = max.Value;
return Math.Max(normalized, 0m);
}
private decimal NormalizePrice(decimal price)
{
var step = Security?.PriceStep;
if (!step.HasValue || step.Value <= 0m)
return price;
var rounded = Math.Round(price / step.Value, MidpointRounding.AwayFromZero) * step.Value;
return rounded;
}
private decimal GetMinStopDistance()
{
var step = Security?.PriceStep ?? 0m;
return step > 0m ? step * MinStopPoints : MinStopPoints;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage, CommodityChannelIndex, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class ma2_cci_ema_strategy(Strategy):
"""
EMA crossover with CCI confirmation and ATR-based stop.
"""
def __init__(self):
super(ma2_cci_ema_strategy, self).__init__()
self._fast_period = self.Param("FastMaPeriod", 10).SetDisplay("Fast EMA", "Fast EMA period", "Indicators")
self._slow_period = self.Param("SlowMaPeriod", 37).SetDisplay("Slow EMA", "Slow EMA period", "Indicators")
self._cci_period = self.Param("CciPeriod", 39).SetDisplay("CCI Period", "CCI length", "Indicators")
self._atr_period = self.Param("AtrPeriod", 3).SetDisplay("ATR Period", "ATR length", "Risk")
self._risk_percent = self.Param("RiskPercent", 2.0)
self._min_stop_points = self.Param("MinStopPoints", 15)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Candles", "General")
self._prev_fast = 0.0
self._prev_slow = 0.0
self._prev_cci = 0.0
self._has_prev = False
self._stop_price = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(ma2_cci_ema_strategy, self).OnReseted()
self._prev_fast = 0.0
self._prev_slow = 0.0
self._prev_cci = 0.0
self._has_prev = False
self._stop_price = None
def OnStarted2(self, time):
super(ma2_cci_ema_strategy, self).OnStarted2(time)
fast = ExponentialMovingAverage()
fast.Length = self._fast_period.Value
slow = ExponentialMovingAverage()
slow.Length = self._slow_period.Value
cci = CommodityChannelIndex()
cci.Length = self._cci_period.Value
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(fast, slow, cci, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, fast)
self.DrawIndicator(area, slow)
self.DrawOwnTrades(area)
def _process_candle(self, candle, fast_val, slow_val, cci_val, atr_val):
if candle.State != CandleStates.Finished:
return
f = float(fast_val)
s = float(slow_val)
c = float(cci_val)
a = float(atr_val)
if not self._has_prev:
self._prev_fast = f
self._prev_slow = s
self._prev_cci = c
self._has_prev = True
return
close = float(candle.ClosePrice)
cross_up = self._prev_fast <= self._prev_slow and f > s
cross_down = self._prev_fast >= self._prev_slow and f < s
cci_up = self._prev_cci <= 0 and c > 0
cci_down = self._prev_cci >= 0 and c < 0
min_stop = self._get_min_stop_distance()
stop_dist = max(a, min_stop)
if self.Position != 0:
exit_triggered = False
if self.Position > 0:
if self._stop_price is not None and float(candle.LowPrice) <= self._stop_price:
self.SellMarket()
exit_triggered = True
elif cross_down:
self.SellMarket()
exit_triggered = True
elif self.Position < 0:
if self._stop_price is not None and float(candle.HighPrice) >= self._stop_price:
self.BuyMarket()
exit_triggered = True
elif cross_up:
self.BuyMarket()
exit_triggered = True
if exit_triggered:
self._stop_price = None
self._prev_fast = f
self._prev_slow = s
self._prev_cci = c
return
else:
if cross_up and cci_up:
self.BuyMarket()
self._stop_price = close - stop_dist
elif cross_down and cci_down:
self.SellMarket()
self._stop_price = close + stop_dist
self._prev_fast = f
self._prev_slow = s
self._prev_cci = c
def _get_min_stop_distance(self):
step = 0.0
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
pts = int(self._min_stop_points.Value)
return step * pts if step > 0 else float(pts)
def CreateClone(self):
return ma2_cci_ema_strategy()