Стратегия представляет собой перенос советника MetaTrader 5 Exp_TDI-2_ReOpen на платформу StockSharp. Торговля ведётся по индикатору Trend Direction Index (TDI-2) с сохранением оригинальной логики повторных входов. Версия на C# использует высокоуровневый API StockSharp: она реагирует на пересечения между линией динамики TDI и индексной линией, добавляет позиции при благоприятном движении цены на заданное количество пунктов и может сопровождать сделки защитными стопами.
Индикаторы
TDI-2 – пользовательский индикатор, реализованный в проекте. Формирует две линии:
Directional – Период × сглаженный импульс, где импульс равен разнице между выбранной ценой и ценой Период баров назад.
Index – |Directional| − (2 × Период × сглаженное |импульса| с длиной 2×Период − |импульс|).
Поддерживаемые методы сглаживания: простое, экспоненциальное, сглаженное (RMA) и линейно-взвешенное среднее.
Набор типов цен полностью повторяет MQL-реализацию, включая формулы TrendFollow и Demark.
Логика торговли
После закрытия каждой свечи считываются значения TDI-2 на свече, заданной параметром Signal Bar (по умолчанию предыдущая закрытая свеча), и ещё на одну свечу глубже.
Если линия Directional находилась выше линии Index и перешла ниже:
При включённом параметре Allow Long Entries и отсутствии длинной позиции готовится новая покупка.
При наличии короткой позиции и разрешённом параметре Allow Short Exits она закрывается.
Если линия Directional находилась ниже линии Index и пересекла её снизу вверх:
При включённом параметре Allow Short Entries и отсутствии короткой позиции формируется продажа.
При наличии длинной позиции и разрешённом параметре Allow Long Exits она закрывается.
Логика повторных входов (масштабирования):
Для длинных позиций контролируется цена последней покупки. Если цена движется в прибыль на величину Re-entry Step (points) и количество совершённых покупок меньше Max Entries, открывается дополнительная покупка базовым объёмом.
Для коротких позиций используется аналогичный алгоритм с последней продажей.
При смене направления стратегия отправляет единственную рыночную заявку, достаточную для закрытия встречной позиции и открытия новой позиции требуемого объёма.
Параметры стоп-лосса и тейк-профита, если заданы, активируются через StartProtection с учётом PriceStep инструмента.
Параметры
Имя
Описание
Значение по умолчанию
Money Management
Базовый объём рыночной заявки.
0.1
Max Entries
Максимальное число входов в одном направлении (включая первый).
10
Stop Loss (points)
Дистанция стоп-лосса в пунктах.
1000
Take Profit (points)
Дистанция тейк-профита в пунктах.
2000
Slippage (points)
Параметр сохранён для совместимости, в данной реализации не используется.
10
Re-entry Step (points)
Минимальное благоприятное движение для добавления к позиции.
300
Allow Long/Short Entries
Разрешение на открытие длинных/коротких позиций.
true
Allow Long/Short Exits
Разрешение на закрытие длинных/коротких позиций.
true
Candle Type
Тип свечей для расчётов.
H4
TDI Smoothing
Метод сглаживания индикатора TDI-2.
Simple MA
TDI Period
Период импульса.
20
TDI Phase
Зарезервирован для совместимости, на поддерживаемые методы не влияет.
15
Applied Price
Тип цены для TDI-2.
Close
Signal Bar
Глубина просмотра закрытых свечей при анализе пересечений.
1
Дополнительные замечания
Реализованы только те методы сглаживания, которые доступны в StockSharp (SMA, EMA, SMMA, LWMA). Экзотические методы MQL (JJMA, T3 и др.) не поддерживаются.
Параметр TDI Phase сохранён для соответствия оригиналу и не влияет на расчёты при используемых методах сглаживания.
Параметр Slippage (points) присутствует для полноты, но высокоуровневый API StockSharp его не применяет.
Счётчики повторных входов автоматически сбрасываются при выходе из позиции.
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Trend Direction Index re-entry strategy.
/// Trades based on crossings between the TDI momentum line and the TDI index line.
/// </summary>
public class Tdi2ReOpenStrategy : Strategy
{
private readonly StrategyParam<int> _tdiPeriod;
private readonly StrategyParam<DataType> _candleType;
private decimal? _lastClose;
private decimal? _directional;
private decimal? _index;
private decimal? _prevDirectional;
private decimal? _prevIndex;
public int TdiPeriod { get => _tdiPeriod.Value; set => _tdiPeriod.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public Tdi2ReOpenStrategy()
{
_tdiPeriod = Param(nameof(TdiPeriod), 10)
.SetGreaterThanZero()
.SetDisplay("TDI Period", "Momentum lookback period", "Indicator")
.SetOptimize(5, 30, 5);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Data series", "General");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_lastClose = null;
_directional = null;
_index = null;
_prevDirectional = null;
_prevIndex = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_lastClose = null;
_directional = null;
_index = null;
_prevDirectional = null;
_prevIndex = null;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var close = candle.ClosePrice;
if (_lastClose is not decimal lastClose)
{
_lastClose = close;
return;
}
var momentum = close - lastClose;
_lastClose = close;
var alpha = 2m / (TdiPeriod + 1m);
if (_directional is not decimal prevDirectionalLine || _index is not decimal prevIndexLine)
{
_directional = momentum;
_index = momentum;
return;
}
var directional = prevDirectionalLine + alpha * (momentum - prevDirectionalLine);
var index = prevIndexLine + alpha * (directional - prevIndexLine);
if (_prevDirectional is not decimal prevDir || _prevIndex is not decimal prevIdx)
{
_directional = directional;
_index = index;
_prevDirectional = prevDirectionalLine;
_prevIndex = prevIndexLine;
return;
}
var crossUp = prevDir <= prevIdx && directional > index;
var crossDown = prevDir >= prevIdx && directional < index;
if (crossUp && Position <= 0)
BuyMarket();
else if (crossDown && Position >= 0)
SellMarket();
_directional = directional;
_index = index;
_prevDirectional = directional;
_prevIndex = index;
}
}
}
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
class tdi_2_re_open_strategy(Strategy):
"""TDI momentum/index crossover: custom EMA-smoothed momentum lines."""
def __init__(self):
super(tdi_2_re_open_strategy, self).__init__()
self._tdi_period = self.Param("TdiPeriod", 10).SetGreaterThanZero().SetDisplay("TDI Period", "Momentum lookback period", "Indicator")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(tdi_2_re_open_strategy, self).OnReseted()
self._last_close = 0
self._directional = None
self._index = None
self._prev_dir = None
self._prev_idx = None
def OnStarted2(self, time):
super(tdi_2_re_open_strategy, self).OnStarted2(time)
self._last_close = 0
self._directional = None
self._index = None
self._prev_dir = None
self._prev_idx = None
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)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
if self._last_close == 0:
self._last_close = close
return
momentum = close - self._last_close
self._last_close = close
alpha = 2.0 / (self._tdi_period.Value + 1.0)
if self._directional is None or self._index is None:
self._directional = momentum
self._index = momentum
return
directional = self._directional + alpha * (momentum - self._directional)
index = self._index + alpha * (directional - self._index)
if self._prev_dir is None or self._prev_idx is None:
self._directional = directional
self._index = index
self._prev_dir = self._directional
self._prev_idx = self._index
return
cross_up = self._prev_dir <= self._prev_idx and directional > index
cross_down = self._prev_dir >= self._prev_idx and directional < index
if cross_up and self.Position <= 0:
self.BuyMarket()
elif cross_down and self.Position >= 0:
self.SellMarket()
self._directional = directional
self._index = index
self._prev_dir = directional
self._prev_idx = index
def CreateClone(self):
return tdi_2_re_open_strategy()