Стратегия T3 MA Direction Change
Общее описание
Данная стратегия повторяет логику советника T3MA(barabashkakvn's edition). Исходный советник использует индикатор «T3MA-ALARM», который дважды экспоненциально сглаживает цены закрытия и формирует сигнал при смене направления сглаженной кривой. Порт на StockSharp реализует тот же подход: рассчитывает EMA от цены закрытия, затем вторую EMA от полученного ряда и реагирует на изменение наклона этого двойного сглаживания.
Стратегия работает только на завершённых свечах. Чтобы воспроизвести параметр InpBarNumber, введена задержка исполнения сигналов на заданное число свечей (по умолчанию одна свеча). Сделки отправляются рыночными приказами, поэтому позиция всегда одна – при смене направления происходит разворот, а не накопление нескольких хеджирующих ордеров.
Торговые правила
- Подписаться на выбранный тип свечей и рассчитать EMA по ценам закрытия. Затем применить вторую EMA к выходным значениям первой EMA, получив ряд для анализа направления.
- Сравнить текущее значение сглаженного ряда (при необходимости сдвинутое на
EMA Shift) с предыдущим. Рост интерпретируется как восходящий наклон, падение – как нисходящий.
- Когда наклон меняется с нисходящего на восходящий – поместить в очередь покупку. При обратном переходе – продажу. Если наклон не изменился, в очередь добавляется нулевой сигнал, чтобы задержка учитывала каждую свечу.
- После истечения заданной задержки
Signal Delay сигнал извлекается из очереди и исполняется. Покупка закрывает текущий шорт (если есть) и открывает лонг базовым объёмом Trade Volume. Продажа аналогично закрывает лонг и открывает шорт.
- Защитные ордера на стоп-лосс и тейк-профит создаются через
StartProtection. Расстояния задаются в шагах цены, поэтому автоматически учитывают минимальный тик выбранного инструмента.
Параметры
| Название |
Описание |
EMA Length |
Длина EMA для обоих этапов сглаживания, аналог параметра MAPeriod в исходном индикаторе. |
EMA Shift |
Сдвиг сглаженного ряда перед сравнением наклонов, соответствует MAShift. |
Signal Delay |
Количество завершённых свечей, через которое исполняется сигнал. Значение 1 повторяет обработку предыдущей свечи как в InpBarNumber. |
Stop Loss (steps) |
Дистанция стоп-лосса в шагах цены. Ноль отключает защиту. |
Take Profit (steps) |
Дистанция тейк-профита в шагах цены. Ноль отключает защиту. |
Trade Volume |
Базовый объём сделки. При развороте к нему добавляется абсолютное значение текущей позиции. |
Candle Type |
Тип свечей, используемый в расчётах (по умолчанию 5-минутные). |
Управление рисками
StartProtection автоматически выставляет уровни стоп-лосса и тейк-профита при запуске и поддерживает их в актуальном состоянии относительно шага цены.
- Переходы выполняются рыночными приказами. Если сигнал совпадает с текущим направлением, дополнительных сделок не совершается, что предотвращает непреднамеренное наращивание позиции.
- Каждая сделка сопровождается записью в журнал с указанием причины и опорной цены из свечи-источника.
Отличия от версии для MQL5
- Оригинальный советник требовал хеджинговый счёт и мог накапливать несколько позиций. В StockSharp используется неттинговый подход – всегда одна позиция, которая разворачивается по сигналу.
- Обработка сигналов выполняется по завершённым свечам, а не по каждому тику, что естественно для высокоуровневого API StockSharp.
- Управление стопами и тейками реализовано через
StartProtection, а не через индивидуальные SL/TP в каждом ордере.
- Код снабжён комментариями на английском языке, параметрами с описаниями и вспомогательной визуализацией.
Рекомендации по применению
- Запускайте стратегию на инструменте с корректно настроенным
PriceStep, чтобы защитные уровни выставлялись на адекватных расстояниях.
- Подберите
EMA Length и значения защитных параметров под волатильность конкретного рынка. Увеличение Signal Delay уменьшает шум, но замедляет реакцию.
- Контролируйте журналы сделок: по ним легко отследить, какая свеча сформировала сигнал и какое направление было принято.
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()