Exp CyclePeriod Strategy
This strategy uses the CyclePeriod indicator to detect market cycle turns. It opens long positions when the indicator rises and short positions when it falls, closing opposite positions accordingly.
Details
- Entry Criteria:
- Long: CyclePeriod is rising and the current value is above the previous one.
- Short: CyclePeriod is falling and the current value is below the previous one.
- Long/Short: Long and Short.
- Exit Criteria:
- Close short when CyclePeriod turns upward.
- Close long when CyclePeriod turns downward.
- Stops: Uses take profit and stop loss in price units.
- Default Values:
CandleType= TimeSpan.FromHours(6).TimeFrame().Alpha= 0.07.SignalBar= 1.TakeProfit= 2000.StopLoss= 1000.BuyPosOpen= true.SellPosOpen= true.BuyPosClose= true.SellPosClose= true.
- Filters:
- Category: Trend following
- Direction: Long & Short
- Indicators: CyclePeriod
- Stops: Yes
- Complexity: Intermediate
- Timeframe: 6-hour
- Seasonality: No
- Neural networks: No
- Divergence: No
- Risk level: Medium
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Trades the Ehlers CyclePeriod indicator (not a fast/slow EMA crossover).
/// The price input is the median of the finished candle's high and low;
/// period estimation follows the published Ehlers phase/instant-period filter.
/// https://www.mql5.com/en/articles/288
/// </summary>
public class ExpCyclePeriodStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _alpha;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<bool> _buyPosOpen;
private readonly StrategyParam<bool> _sellPosOpen;
private readonly StrategyParam<bool> _buyPosClose;
private readonly StrategyParam<bool> _sellPosClose;
private readonly List<double> _prices = [];
private readonly List<double> _smooth = [];
private readonly List<double> _cycle = [];
private readonly List<double> _quadrature = [];
private readonly List<double> _inPhase = [];
private readonly List<double> _deltaPhase = [];
private readonly List<double> _instantPeriod = [];
private readonly List<double> _cyclePeriod = [];
private int _validPhaseCount;
private Order _pendingOrder;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public decimal Alpha { get => _alpha.Value; set => _alpha.Value = value; }
public int SignalBar { get => _signalBar.Value; set => _signalBar.Value = value; }
public decimal TakeProfit { get => _takeProfit.Value; set => _takeProfit.Value = value; }
public decimal StopLoss { get => _stopLoss.Value; set => _stopLoss.Value = value; }
public bool BuyPosOpen { get => _buyPosOpen.Value; set => _buyPosOpen.Value = value; }
public bool SellPosOpen { get => _sellPosOpen.Value; set => _sellPosOpen.Value = value; }
public bool BuyPosClose { get => _buyPosClose.Value; set => _buyPosClose.Value = value; }
public bool SellPosClose { get => _sellPosClose.Value; set => _sellPosClose.Value = value; }
public ExpCyclePeriodStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(6).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
_alpha = Param(nameof(Alpha), 0.07m).SetGreaterThanZero()
.SetDisplay("Alpha", "Ehlers cycle filter coefficient", "Indicator");
_signalBar = Param(nameof(SignalBar), 1).SetGreaterThanZero()
.SetDisplay("Signal Bar", "Cycle-period comparison lag", "Indicator");
_takeProfit = Param(nameof(TakeProfit), 2000m).SetGreaterThanZero()
.SetDisplay("Take Profit", "Absolute price-distance target", "Risk");
_stopLoss = Param(nameof(StopLoss), 1000m).SetGreaterThanZero()
.SetDisplay("Stop Loss", "Absolute price-distance stop", "Risk");
_buyPosOpen = Param(nameof(BuyPosOpen), true).SetDisplay("Buy Open", "Allow long entries", "Logic");
_sellPosOpen = Param(nameof(SellPosOpen), true).SetDisplay("Sell Open", "Allow short entries", "Logic");
_buyPosClose = Param(nameof(BuyPosClose), true).SetDisplay("Buy Close", "Allow long closes", "Logic");
_sellPosClose = Param(nameof(SellPosClose), true).SetDisplay("Sell Close", "Allow short closes", "Logic");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
private void ResetCycle()
{
_prices.Clear(); _smooth.Clear(); _cycle.Clear(); _quadrature.Clear(); _inPhase.Clear();
_deltaPhase.Clear(); _instantPeriod.Clear(); _cyclePeriod.Clear();
_validPhaseCount = 0;
_pendingOrder = null;
}
protected override void OnReseted()
{
base.OnReseted();
ResetCycle();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetCycle();
StartProtection(new Unit(TakeProfit, UnitTypes.Absolute), new Unit(StopLoss, UnitTypes.Absolute));
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
}
private double UpdateCycle(double price)
{
var a = (double)Alpha;
var n = _prices.Count;
_prices.Add(price);
var smooth = n >= 3
? (price + 2 * _prices[n - 1] + 2 * _prices[n - 2] + _prices[n - 3]) / 6.0
: price;
_smooth.Add(smooth);
double cycle;
if (n < 2)
cycle = 0;
else if (n < 7)
cycle = (price - 2 * _prices[n - 1] + _prices[n - 2]) / 4.0;
else
cycle = Math.Pow(1 - 0.5 * a, 2) * (smooth - 2 * _smooth[n - 1] + _smooth[n - 2])
+ 2 * (1 - a) * _cycle[n - 1] - Math.Pow(1 - a, 2) * _cycle[n - 2];
_cycle.Add(cycle);
var previousInstant = n == 0 ? 0.0 : _instantPeriod[n - 1];
var q = n >= 6
? (0.0962 * cycle + 0.5769 * _cycle[n - 2] - 0.5769 * _cycle[n - 4] - 0.0962 * _cycle[n - 6])
* (0.5 + 0.08 * previousInstant)
: 0.0;
var i = n >= 3 ? _cycle[n - 3] : 0.0;
double delta = 0;
if (n >= 7 && q != 0.0 && _quadrature[n - 1] != 0.0)
{
var oldQ = _quadrature[n - 1];
var oldI = _inPhase[n - 1];
var denominator = 1.0 + i * oldI / (q * oldQ);
if (denominator != 0.0)
{
delta = ((i / q) - (oldI / oldQ)) / denominator;
_validPhaseCount++;
}
}
delta = Math.Clamp(delta, 0.1, 0.9);
_quadrature.Add(q);
_inPhase.Add(i);
_deltaPhase.Add(delta);
double instant = 0, period = 0;
if (n >= 7)
{
var recent = new double[5];
for (var k = 0; k < 5; k++)
recent[k] = _deltaPhase[n - 4 + k];
Array.Sort(recent);
var median = recent[2];
var dominant = median == 0.0 ? 15.0 : 6.28318 / median + 0.5;
instant = 0.33 * dominant + 0.67 * previousInstant;
period = 0.15 * instant + 0.85 * _cyclePeriod[n - 1];
}
_instantPeriod.Add(instant);
_cyclePeriod.Add(period);
return period;
}
private bool HasPendingOrder => _pendingOrder is not null &&
_pendingOrder.State is not (OrderStates.Done or OrderStates.Failed);
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var current = UpdateCycle((double)((candle.HighPrice + candle.LowPrice) / 2m));
var lag = SignalBar;
if (lag < 1 || _cyclePeriod.Count < Math.Max(15, lag + 8) ||
_validPhaseCount < 5 || HasPendingOrder || !IsFormedAndOnlineAndAllowTrading())
return;
var previous = _cyclePeriod[_cyclePeriod.Count - 1 - lag];
if (current > previous)
{
if (Position < 0m && SellPosClose)
_pendingOrder = BuyMarket(Math.Abs(Position) + (BuyPosOpen ? Volume : 0m));
else if (Position == 0m && BuyPosOpen)
_pendingOrder = BuyMarket(Volume);
}
else if (current < previous)
{
if (Position > 0m && BuyPosClose)
_pendingOrder = SellMarket(Math.Abs(Position) + (SellPosOpen ? Volume : 0m));
else if (Position == 0m && SellPosOpen)
_pendingOrder = SellMarket(Volume);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes, OrderStates
from StockSharp.Algo.Strategies import Strategy
class exp_cycle_period_strategy(Strategy):
"""Ehlers' CyclePeriod, not a fast/slow EMA crossover.
Core recursion is based on Ehlers Cycle Period (CyclePeriod.mq5):
https://www.mql5.com/en/articles/288
Price input is median of finished candle high/low. SignalBar compares a
formed cycle-period reading against its N-bar-old reading.
"""
def __init__(self):
super(exp_cycle_period_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(6))).SetDisplay(
"Candle Type", "Candle timeframe", "General")
self._alpha = self.Param("Alpha", 0.07).SetDisplay("Alpha", "Ehlers cycle filter coefficient", "Indicator")
self._signal_bar = self.Param("SignalBar", 1).SetDisplay("Signal Bar", "Cycle period comparison lag", "Indicator")
self._take_profit = self.Param("TakeProfit", 2000.0).SetDisplay("Take Profit", "Absolute price-distance target", "Risk")
self._stop_loss = self.Param("StopLoss", 1000.0).SetDisplay("Stop Loss", "Absolute price-distance stop", "Risk")
self._buy_open = self.Param("BuyPosOpen", True).SetDisplay("Buy Open", "Allow long entries", "Logic")
self._sell_open = self.Param("SellPosOpen", True).SetDisplay("Sell Open", "Allow short entries", "Logic")
self._buy_close = self.Param("BuyPosClose", True).SetDisplay("Buy Close", "Allow long closes", "Logic")
self._sell_close = self.Param("SellPosClose", True).SetDisplay("Sell Close", "Allow short closes", "Logic")
self._reset_state()
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
@property
def Alpha(self): return self._alpha.Value
@Alpha.setter
def Alpha(self, value): self._alpha.Value = value
@property
def SignalBar(self): return self._signal_bar.Value
@SignalBar.setter
def SignalBar(self, value): self._signal_bar.Value = value
@property
def TakeProfit(self): return self._take_profit.Value
@TakeProfit.setter
def TakeProfit(self, value): self._take_profit.Value = value
@property
def StopLoss(self): return self._stop_loss.Value
@StopLoss.setter
def StopLoss(self, value): self._stop_loss.Value = value
@property
def BuyPosOpen(self): return self._buy_open.Value
@BuyPosOpen.setter
def BuyPosOpen(self, value): self._buy_open.Value = value
@property
def SellPosOpen(self): return self._sell_open.Value
@SellPosOpen.setter
def SellPosOpen(self, value): self._sell_open.Value = value
@property
def BuyPosClose(self): return self._buy_close.Value
@BuyPosClose.setter
def BuyPosClose(self, value): self._buy_close.Value = value
@property
def SellPosClose(self): return self._sell_close.Value
@SellPosClose.setter
def SellPosClose(self, value): self._sell_close.Value = value
def _reset_state(self):
self._price = []
self._smooth = []
self._cycle = []
self._q = []
self._i = []
self._phase = []
self._inst = []
self._period = []
self._valid_phase_count = 0
self._pending_order = None
def OnReseted(self):
super(exp_cycle_period_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(exp_cycle_period_strategy, self).OnStarted2(time)
self._reset_state()
self.StartProtection(
takeProfit=Unit(self.TakeProfit, UnitTypes.Absolute),
stopLoss=Unit(self.StopLoss, UnitTypes.Absolute))
self.SubscribeCandles(self.CandleType).Bind(self.ProcessCandle).Start()
def _compute_period(self, price):
alpha = float(self.Alpha)
n = len(self._price)
self._price.append(float(price))
if n >= 3:
smooth = (self._price[n] + 2*self._price[n-1] + 2*self._price[n-2] + self._price[n-3]) / 6.0
else:
smooth = self._price[n]
self._smooth.append(smooth)
if n < 2:
cycle = 0.0
elif n < 7:
cycle = (self._price[n] - 2*self._price[n-1] + self._price[n-2]) / 4.0
else:
cycle = ((1-0.5*alpha)**2 * (smooth - 2*self._smooth[n-1] + self._smooth[n-2])
+ 2*(1-alpha)*self._cycle[n-1] - (1-alpha)**2*self._cycle[n-2])
self._cycle.append(cycle)
prev_inst = self._inst[-1] if self._inst else 0.0
q = ((0.0962*cycle + 0.5769*self._cycle[n-2]
- 0.5769*self._cycle[n-4] - 0.0962*self._cycle[n-6]) * (0.5+0.08*prev_inst)) if n >= 6 else 0.0
i = self._cycle[n-3] if n >= 3 else 0.0
delta = 0.0
if n >= 7 and q != 0.0 and self._q[-1] != 0.0:
previous_q, previous_i = self._q[-1], self._i[-1]
den = 1.0 + i*previous_i/(q*previous_q)
if den != 0.0:
delta = ((i/q) - (previous_i/previous_q)) / den
self._valid_phase_count += 1
delta = max(0.1, min(0.9, delta))
self._q.append(q); self._i.append(i); self._phase.append(delta)
if n >= 7:
recent = sorted(self._phase[-5:])
median = recent[2]
dominant = (6.28318 / median + 0.5) if median else 15.0
inst = 0.33*dominant + 0.67*prev_inst
previous_period = self._period[-1] if self._period else 0.0
period = 0.15*inst + 0.85*previous_period
else:
inst = period = 0.0
self._inst.append(inst)
self._period.append(period)
return period
def _has_pending(self):
return self._pending_order is not None and self._pending_order.State not in (
OrderStates.Done, OrderStates.Failed)
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
price = (float(candle.HighPrice) + float(candle.LowPrice)) / 2.0
period = self._compute_period(price)
lag = int(self.SignalBar)
if (lag < 1 or len(self._period) < max(15, lag+8) or self._valid_phase_count < 5 or
self._has_pending() or not self.IsFormedAndOnlineAndAllowTrading()):
return
prior = self._period[-1-lag]
if period > prior:
if self.Position < 0:
if self.SellPosClose:
volume = abs(self.Position) + (self.Volume if self.BuyPosOpen else 0)
self._pending_order = self.BuyMarket(volume)
elif self.Position == 0 and self.BuyPosOpen:
self._pending_order = self.BuyMarket(self.Volume)
elif period < prior:
if self.Position > 0:
if self.BuyPosClose:
volume = abs(self.Position) + (self.Volume if self.SellPosOpen else 0)
self._pending_order = self.SellMarket(volume)
elif self.Position == 0 and self.SellPosOpen:
self._pending_order = self.SellMarket(self.Volume)
def CreateClone(self):
return exp_cycle_period_strategy()