Cyclops Cycle Identifier Strategy
Overview
This strategy ports the MetaTrader expert advisor Cyclops v1.2 together with its proprietary CycleIdentifier indicator to StockSharp's high level API. The algorithm smooths closing prices with a smoothed moving average (SMMA), measures recent volatility through a long lookback average true range, and marks cycle turning points when price travels far enough from the most recent swing. Major cycle reversals generate new entries while minor reversals offer optional exit signals.
A configurable zero-lag filter validates the slope of the smoothed series. The filter can work directly on smoothed price data or on a Wilder-style RSI derived from the same series. Additional confirmation is available through a classic Momentum indicator, and trading can be limited to a specific weekday/hour window.
Signal logic
- Cycle detection – The internal state machine tracks the last swing highs and lows of the smoothed price. When price travels beyond the adaptive threshold (average range × Length), the strategy marks a minor cycle. A larger multiple (MajorCycleStrength) is required to flag a major cycle.
- Entries – Major bullish cycles (
MajorBuy) open longs; major bearish cycles (MajorSell) open shorts. Active positions are automatically closed before reversing to the opposite side.
- Optional exits – When UseExitSignal is enabled, profitable trades can close on the corresponding minor cycle signal (
MinorSellExit for longs, MinorBuyExit for shorts) if no opposite major cycle is present.
- Zero-lag filter – If UseCycleFilter is enabled, a zero-lag smoothing filter must confirm the slope (rising for longs, falling for shorts). The filter source is selected by CycleFilterMode (smoothed price or RSI).
- Momentum filter – With UseMomentumFilter enabled, entries require
Momentum ≥ MomentumTriggerLong for longs and Momentum ≤ MomentumTriggerShort for shorts.
Trade management
- Fixed targets – TakeProfitPips and StopLossPips define optional fixed exits in instrument pips.
- Break-even – When BreakEvenTrigger pips of profit are reached, the stop is pulled to entry ± one pip.
- Trailing – TrailingStopTrigger activates a trailing stop that follows price at TrailingStopPips once the trigger distance is achieved.
- Session control – If UseTimeRestriction is true, new positions are allowed only before
DayEnd (0=Sunday) and up to HourEnd (inclusive) on that day. Existing trades are still managed afterward.
Parameters
| Parameter |
Description |
Volume |
Order volume used for entries. |
PriceActionFilter |
Length of the smoothed moving average applied to close price. |
Length |
Multiplier applied to the average range to detect minor cycles. |
MajorCycleStrength |
Multiplier separating major from minor swings. |
UseCycleFilter |
Enables the zero-lag slope confirmation. |
CycleFilterMode |
Selects zero-lag input: smoothed price (Sma) or RSI (Rsi). |
FilterStrengthSma |
Length of the zero-lag filter when the smoothed price is used. |
FilterStrengthRsi |
Length and RSI period when the filter relies on RSI values. |
UseMomentumFilter |
Turns the momentum confirmation on or off. |
MomentumPeriod |
Momentum indicator length. |
MomentumTriggerLong |
Minimum momentum required for long entries. |
MomentumTriggerShort |
Maximum momentum allowed for short entries. |
UseExitSignal |
Enables minor-cycle based exits when profitable. |
UseTimeRestriction |
Limits trading to the configured weekday/hour window. |
DayEnd |
Last day of week when new entries are permitted. |
HourEnd |
Last hour on the final trading day for new entries. |
BreakEvenTrigger |
Profit in pips required to activate the break-even stop. |
TrailingStopTrigger |
Profit in pips required to start trailing. |
TrailingStopPips |
Distance in pips maintained by the trailing stop. |
TakeProfitPips |
Fixed take-profit distance in pips. |
StopLossPips |
Fixed stop-loss distance in pips. |
CandleType |
Primary timeframe that feeds the strategy. |
Differences compared to the original EA
- The average range is estimated with a 250-period Average True Range multiplied by Length, providing behaviour equivalent to the rolling high/low span used in MQL.
- The momentum confirmation uses the actual indicator value (the MQL script compared against the pip multiplier
bm, effectively disabling the filter).
- Zero-lag smoothing is implemented with the same recursive coefficients but expressed in decimal arithmetic. RSI mode uses a Wilder RSI whose period equals FilterStrengthRsi.
Usage notes
- Select the instrument and bind the
CandleType parameter to the desired timeframe.
- Configure the risk and session settings to match your broker environment.
- Enable UseCycleFilter or UseMomentumFilter when a stricter confirmation is required; disable them for faster but noisier entries.
- The strategy maintains at most one open position. Opposite cycle signals close the current position before a new one is evaluated.
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Cyclops CycleIdentifier: EMA crossover with RSI confirmation and ATR stops.
/// </summary>
public class CyclopsCycleIdentifierStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _fastEmaLength;
private readonly StrategyParam<int> _slowEmaLength;
private readonly StrategyParam<int> _rsiLength;
private readonly StrategyParam<int> _atrLength;
private decimal _prevFast;
private decimal _prevSlow;
private decimal _entryPrice;
public CyclopsCycleIdentifierStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
.SetDisplay("Candle Type", "Timeframe.", "General");
_fastEmaLength = Param(nameof(FastEmaLength), 8)
.SetDisplay("Fast EMA Length", "Fast EMA period.", "Indicators");
_slowEmaLength = Param(nameof(SlowEmaLength), 21)
.SetDisplay("Slow EMA Length", "Slow EMA period.", "Indicators");
_rsiLength = Param(nameof(RsiLength), 14)
.SetDisplay("RSI Length", "RSI period.", "Indicators");
_atrLength = Param(nameof(AtrLength), 14)
.SetDisplay("ATR Length", "ATR period.", "Indicators");
}
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int FastEmaLength { get => _fastEmaLength.Value; set => _fastEmaLength.Value = value; }
public int SlowEmaLength { get => _slowEmaLength.Value; set => _slowEmaLength.Value = value; }
public int RsiLength { get => _rsiLength.Value; set => _rsiLength.Value = value; }
public int AtrLength { get => _atrLength.Value; set => _atrLength.Value = value; }
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevFast = 0; _prevSlow = 0; _entryPrice = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevFast = 0; _prevSlow = 0; _entryPrice = 0;
var fastEma = new ExponentialMovingAverage { Length = FastEmaLength };
var slowEma = new ExponentialMovingAverage { Length = SlowEmaLength };
var rsi = new RelativeStrengthIndex { Length = RsiLength };
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(fastEma, slowEma, rsi, atr, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null) { DrawCandles(area, subscription); DrawIndicator(area, fastEma); DrawIndicator(area, slowEma); DrawOwnTrades(area); }
}
private void ProcessCandle(ICandleMessage candle, decimal fastVal, decimal slowVal, decimal rsiVal, decimal atrVal)
{
if (candle.State != CandleStates.Finished) return;
if (_prevFast == 0 || _prevSlow == 0 || atrVal <= 0) { _prevFast = fastVal; _prevSlow = slowVal; return; }
var close = candle.ClosePrice;
if (Position > 0)
{
if ((fastVal < slowVal && _prevFast >= _prevSlow) || close <= _entryPrice - atrVal * 2m) { SellMarket(); _entryPrice = 0; }
}
else if (Position < 0)
{
if ((fastVal > slowVal && _prevFast <= _prevSlow) || close >= _entryPrice + atrVal * 2m) { BuyMarket(); _entryPrice = 0; }
}
if (Position == 0)
{
if (fastVal > slowVal && _prevFast <= _prevSlow && rsiVal > 50) { _entryPrice = close; BuyMarket(); }
else if (fastVal < slowVal && _prevFast >= _prevSlow && rsiVal < 50) { _entryPrice = close; SellMarket(); }
}
_prevFast = fastVal; _prevSlow = slowVal;
}
}
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.Strategies import Strategy
from StockSharp.Algo.Indicators import ExponentialMovingAverage, RelativeStrengthIndex, AverageTrueRange
class cyclops_cycle_identifier_strategy(Strategy):
def __init__(self):
super(cyclops_cycle_identifier_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(2))) \
.SetDisplay("Candle Type", "Timeframe.", "General")
self._fast_ema_length = self.Param("FastEmaLength", 8) \
.SetDisplay("Fast EMA Length", "Fast EMA period.", "Indicators")
self._slow_ema_length = self.Param("SlowEmaLength", 21) \
.SetDisplay("Slow EMA Length", "Slow EMA period.", "Indicators")
self._rsi_length = self.Param("RsiLength", 14) \
.SetDisplay("RSI Length", "RSI period.", "Indicators")
self._atr_length = self.Param("AtrLength", 14) \
.SetDisplay("ATR Length", "ATR period.", "Indicators")
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@property
def FastEmaLength(self):
return self._fast_ema_length.Value
@property
def SlowEmaLength(self):
return self._slow_ema_length.Value
@property
def RsiLength(self):
return self._rsi_length.Value
@property
def AtrLength(self):
return self._atr_length.Value
def OnStarted2(self, time):
super(cyclops_cycle_identifier_strategy, self).OnStarted2(time)
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._fast_ema = ExponentialMovingAverage()
self._fast_ema.Length = self.FastEmaLength
self._slow_ema = ExponentialMovingAverage()
self._slow_ema.Length = self.SlowEmaLength
self._rsi = RelativeStrengthIndex()
self._rsi.Length = self.RsiLength
self._atr = AverageTrueRange()
self._atr.Length = self.AtrLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._fast_ema, self._slow_ema, self._rsi, self._atr, self.ProcessCandle).Start()
def ProcessCandle(self, candle, fast_val, slow_val, rsi_val, atr_val):
if candle.State != CandleStates.Finished:
return
fv = float(fast_val)
sv = float(slow_val)
rv = float(rsi_val)
av = float(atr_val)
if self._prev_fast == 0 or self._prev_slow == 0 or av <= 0:
self._prev_fast = fv
self._prev_slow = sv
return
close = float(candle.ClosePrice)
if self.Position > 0:
if (fv < sv and self._prev_fast >= self._prev_slow) or close <= self._entry_price - av * 2.0:
self.SellMarket()
self._entry_price = 0.0
elif self.Position < 0:
if (fv > sv and self._prev_fast <= self._prev_slow) or close >= self._entry_price + av * 2.0:
self.BuyMarket()
self._entry_price = 0.0
if self.Position == 0:
if fv > sv and self._prev_fast <= self._prev_slow and rv > 50:
self._entry_price = close
self.BuyMarket()
elif fv < sv and self._prev_fast >= self._prev_slow and rv < 50:
self._entry_price = close
self.SellMarket()
self._prev_fast = fv
self._prev_slow = sv
def OnReseted(self):
super(cyclops_cycle_identifier_strategy, self).OnReseted()
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
def CreateClone(self):
return cyclops_cycle_identifier_strategy()