Стратегия Exp CyclePeriod
Стратегия использует индикатор CyclePeriod для определения разворотов рыночного цикла. Открывает покупки при росте индикатора и продажи при его падении, закрывая противоположные позиции.
Подробности
- Критерии входа:
- Покупка: CyclePeriod растёт и текущее значение выше предыдущего.
- Продажа: CyclePeriod падает и текущее значение ниже предыдущего.
- Направление: Покупка и продажа.
- Критерии выхода:
- Закрыть продажу при развороте CyclePeriod вверх.
- Закрыть покупку при развороте CyclePeriod вниз.
- Стопы: Используются тейк-профит и стоп-лосс в ценовых единицах.
- Параметры по умолчанию:
CandleType= TimeSpan.FromHours(6).TimeFrame().Alpha= 0.07.SignalBar= 1.TakeProfit= 2000.StopLoss= 1000.BuyPosOpen= true.SellPosOpen= true.BuyPosClose= true.SellPosClose= true.
- Фильтры:
- Категория: Следование тренду
- Направление: Покупка и продажа
- Индикаторы: CyclePeriod
- Стопы: Да
- Сложность: Средняя
- Таймфрейм: 6 часов
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
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()