namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Strategy that trades when a double-smoothed EMA changes its slope direction.
/// </summary>
public class T3MaDirectionChangeStrategy : Strategy
{
private readonly StrategyParam<int> _maLength;
private readonly StrategyParam<int> _maShift;
private readonly StrategyParam<int> _signalBarOffset;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<int> _signalCooldownBars;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _recentSmoothed = new();
private readonly Queue<SignalInfo> _pendingSignals = new();
private ExponentialMovingAverage _emaPrice;
private ExponentialMovingAverage _emaSmooth;
private int _previousDirection;
private int _cooldownRemaining;
public int MaLength { get => _maLength.Value; set => _maLength.Value = value; }
public int MaShift { get => _maShift.Value; set => _maShift.Value = value; }
public int SignalBarOffset { get => _signalBarOffset.Value; set => _signalBarOffset.Value = value; }
public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
public decimal TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
public decimal TradeVolume { get => _tradeVolume.Value; set => _tradeVolume.Value = value; }
public int SignalCooldownBars { get => _signalCooldownBars.Value; set => _signalCooldownBars.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public T3MaDirectionChangeStrategy()
{
_maLength = Param(nameof(MaLength), 4)
.SetGreaterThanZero()
.SetDisplay("EMA Length", "Length of the EMA used for the double smoothing", "Indicator");
_maShift = Param(nameof(MaShift), 0)
.SetNotNegative()
.SetDisplay("EMA Shift", "Shift applied to the smoothed EMA when evaluating slope changes", "Indicator");
_signalBarOffset = Param(nameof(SignalBarOffset), 1)
.SetNotNegative()
.SetDisplay("Signal Delay", "How many completed candles to wait before acting on a signal", "Trading rules");
_stopLossPoints = Param(nameof(StopLossPoints), 20m)
.SetNotNegative()
.SetDisplay("Stop Loss (steps)", "Stop loss distance expressed in price steps", "Risk management");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 125m)
.SetNotNegative()
.SetDisplay("Take Profit (steps)", "Take profit distance expressed in price steps", "Risk management");
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Base volume used for entries", "Trading rules");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 12)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown", "Bars to wait after entries and exits", "Trading rules");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Type of candles used for calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_emaPrice = null;
_emaSmooth = null;
_recentSmoothed.Clear();
_pendingSignals.Clear();
_previousDirection = 0;
_cooldownRemaining = 0;
Volume = TradeVolume;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = TradeVolume;
_emaPrice = new EMA { Length = MaLength };
_emaSmooth = new EMA { Length = MaLength };
_recentSmoothed.Clear();
_pendingSignals.Clear();
_previousDirection = 0;
_cooldownRemaining = 0;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
var slUnit = StopLossPoints > 0m ? new Unit(StopLossPoints, UnitTypes.Absolute) : null;
var tpUnit = TakeProfitPoints > 0m ? new Unit(TakeProfitPoints, UnitTypes.Absolute) : null;
StartProtection(slUnit, tpUnit);
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var emaPriceValue = _emaPrice.Process(new DecimalIndicatorValue(_emaPrice, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
var emaSmoothValue = _emaSmooth.Process(emaPriceValue);
if (!emaSmoothValue.IsFormed)
return;
AddSmoothedValue(emaSmoothValue.ToDecimal(), MaShift + 2);
if (_recentSmoothed.Count < MaShift + 2)
{
EnqueueSignal(new SignalInfo(0));
return;
}
var currentIndex = _recentSmoothed.Count - 1 - MaShift;
var previousIndex = _recentSmoothed.Count - 2 - MaShift;
var current = _recentSmoothed[currentIndex];
var previous = _recentSmoothed[previousIndex];
var direction = _previousDirection;
if (current > previous)
direction = 1;
else if (current < previous)
direction = -1;
var signal = 0;
if (_previousDirection == -1 && direction == 1)
signal = 1;
else if (_previousDirection == 1 && direction == -1)
signal = -1;
_previousDirection = direction;
EnqueueSignal(new SignalInfo(signal));
}
private void AddSmoothedValue(decimal value, int limit)
{
_recentSmoothed.Add(value);
if (_recentSmoothed.Count > limit)
_recentSmoothed.RemoveAt(0);
}
private void EnqueueSignal(SignalInfo signal)
{
_pendingSignals.Enqueue(signal);
while (_pendingSignals.Count > SignalBarOffset)
{
var readySignal = _pendingSignals.Dequeue();
ExecuteSignal(readySignal);
}
}
private void ExecuteSignal(SignalInfo signal)
{
if (signal.Direction == 0 || _cooldownRemaining > 0)
return;
if (signal.Direction > 0 && Position <= 0)
{
var volume = Volume + Math.Abs(Position);
BuyMarket(volume);
_cooldownRemaining = SignalCooldownBars;
}
else if (signal.Direction < 0 && Position >= 0)
{
var volume = Volume + Math.Abs(Position);
SellMarket(volume);
_cooldownRemaining = SignalCooldownBars;
}
}
private readonly record struct SignalInfo(int Direction);
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class t3_ma_direction_change_strategy(Strategy):
"""Double-smoothed EMA slope direction change with signal delay and StartProtection."""
def __init__(self):
super(t3_ma_direction_change_strategy, self).__init__()
self._ma_length = self.Param("MaLength", 4).SetGreaterThanZero().SetDisplay("EMA Length", "Length of EMA for double smoothing", "Indicator")
self._ma_shift = self.Param("MaShift", 0).SetNotNegative().SetDisplay("EMA Shift", "Shift applied to smoothed EMA", "Indicator")
self._signal_bar_offset = self.Param("SignalBarOffset", 1).SetNotNegative().SetDisplay("Signal Delay", "Candles to wait before acting on signal", "Trading rules")
self._sl_points = self.Param("StopLossPoints", 20.0).SetNotNegative().SetDisplay("Stop Loss (steps)", "SL distance in price steps", "Risk management")
self._tp_points = self.Param("TakeProfitPoints", 125.0).SetNotNegative().SetDisplay("Take Profit (steps)", "TP distance in price steps", "Risk management")
self._cooldown = self.Param("SignalCooldownBars", 12).SetGreaterThanZero().SetDisplay("Signal Cooldown", "Bars to wait after entries/exits", "Trading rules")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))).SetDisplay("Candle Type", "Type of candles", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(t3_ma_direction_change_strategy, self).OnReseted()
self._recent_smoothed = []
self._pending_signals = []
self._prev_direction = 0
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(t3_ma_direction_change_strategy, self).OnStarted2(time)
self._recent_smoothed = []
self._pending_signals = []
self._prev_direction = 0
self._cooldown_remaining = 0
self._ema_price = ExponentialMovingAverage()
self._ema_price.Length = self._ma_length.Value
self._ema_smooth = ExponentialMovingAverage()
self._ema_smooth.Length = self._ma_length.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
sl_val = float(self._sl_points.Value)
tp_val = float(self._tp_points.Value)
sl_unit = Unit(sl_val, UnitTypes.Absolute) if sl_val > 0 else None
tp_unit = Unit(tp_val, UnitTypes.Absolute) if tp_val > 0 else None
self.StartProtection(sl_unit, tp_unit)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
ema_price_result = process_float(self._ema_price, candle.ClosePrice, candle.OpenTime, True)
ema_smooth_result = self._ema_smooth.Process(ema_price_result)
if not ema_smooth_result.IsFormed:
self._enqueue_signal(0)
return
smoothed_val = float(ema_smooth_result)
shift = self._ma_shift.Value
required = shift + 2
self._recent_smoothed.append(smoothed_val)
if len(self._recent_smoothed) > required:
self._recent_smoothed.pop(0)
if len(self._recent_smoothed) < required:
self._enqueue_signal(0)
return
current_idx = len(self._recent_smoothed) - 1 - shift
prev_idx = len(self._recent_smoothed) - 2 - shift
current = self._recent_smoothed[current_idx]
previous = self._recent_smoothed[prev_idx]
if current > previous:
direction = 1
elif current < previous:
direction = -1
else:
direction = self._prev_direction
signal = 0
if self._prev_direction == -1 and direction == 1:
signal = 1
elif self._prev_direction == 1 and direction == -1:
signal = -1
self._prev_direction = direction
self._enqueue_signal(signal)
def _enqueue_signal(self, signal):
self._pending_signals.append(signal)
offset = self._signal_bar_offset.Value
while len(self._pending_signals) > offset:
ready = self._pending_signals.pop(0)
self._execute_signal(ready)
def _execute_signal(self, direction):
if direction == 0 or self._cooldown_remaining > 0:
return
if direction > 0 and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._cooldown_remaining = self._cooldown.Value
elif direction < 0 and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._cooldown_remaining = self._cooldown.Value
def CreateClone(self):
return t3_ma_direction_change_strategy()