Стратегия двойного подтверждения тренда скользящими средними
Общее описание
Стратегия двойного подтверждения тренда повторяет оригинального советника MetaTrader, который сочетает медленную экспоненциальную скользящую среднюю (EMA) и быструю линейно-взвешенную среднюю (LWMA). Сделки открываются только тогда, когда обе скользящие средние последовательно наклонены в одну сторону, а закрытие предыдущей свечи подтверждает направление движения. Такой подход позволяет входить в наиболее сильные импульсные участки тренда.
Реализация на StockSharp обрабатывает исключительно закрытые свечи, хранит наклон обеих средних за последние три бара и автоматически управляет защитными приказами через StartProtection. Стратегия не зависит от конкретного инструмента — требуется лишь наличие свечей и определённого шага цены.
Индикаторы
- Медленная EMA (по умолчанию 57) — фильтрует глобальный тренд и должна расти/падать две свечи подряд.
- Быстрая LWMA (по умолчанию 3) — подтверждает импульс. Её направление должно совпадать с направлением медленной EMA.
Параметры
| Параметр |
Значение по умолчанию |
Описание |
SlowMaLength |
57 |
Период медленной EMA. |
FastMaLength |
3 |
Период быстрой LWMA. |
StopLossPoints |
100 |
Размер стоп-лосса в пунктах инструмента (переводится в цену через Security.PriceStep). |
TakeProfitPoints |
100 |
Размер тейк-профита в пунктах инструмента (переводится в цену через Security.PriceStep). |
CandleType |
15 минут |
Тип свечей, используемый в расчётах. |
Все параметры описаны через StrategyParam<T>, поэтому их можно менять на лету или использовать в оптимизации.
Правила входа и выхода
Покупка
- EMA растёт: текущее значение > предыдущее > значение два бара назад.
- LWMA растёт: текущее значение > предыдущее > значение два бара назад.
- Закрытие предыдущей свечи выше значения EMA на предыдущем баре.
- Текущая EMA выше текущей LWMA.
- Текущая позиция отсутствует или короткая.
- При выполнении всех условий отправляется рыночная заявка на покупку объёмом
Volume + |Position| для переворота в длинную позицию.
Продажа
- EMA падает: текущее значение < предыдущее < значение два бара назад.
- LWMA падает: текущее значение < предыдущее < значение два бара назад.
- Закрытие предыдущей свечи ниже значения EMA на предыдущем баре.
- Текущая EMA ниже текущей LWMA.
- Текущая позиция отсутствует или длинная.
- При выполнении условий отправляется рыночная заявка на продажу объёмом
Volume + |Position| для переворота в короткую позицию.
Защитные механизмы
StartProtection преобразует значения стоп-лосса и тейк-профита из пунктов в абсолютную цену, умножая их на Security.PriceStep, и выставляет защитные приказы рыночным способом.
- При появлении обратного сигнала стратегия сразу разворачивает позицию, независимо от состояния защитных заявок.
Реализация
- В расчётах участвуют только свечи со статусом
CandleStates.Finished, что эквивалентно проверке нового бара в MQL.
- Внутренние поля хранят последние два значения индикаторов и предыдущее закрытие, поэтому не требуется обращаться к истории индикаторов.
- Метод
IsFormedAndOnlineAndAllowTrading() гарантирует, что торговля ведётся только при наличии всех данных и разрешённом состоянии стратегии.
- Записи
LogInfo подробно фиксируют события входа, что упрощает отладку и мониторинг.
- При наличии графической панели рисуются свечи и обе скользящие средние для визуальной проверки.
Рекомендации по использованию
- Настройте
Volume в соответствии с лотностью инструмента — стратегия всегда отправляет ордера объёмом Volume + |Position|.
- Если у инструмента не задан
PriceStep, код использует значение 1. В этом случае параметры стопов и профитов стоит подстроить вручную.
- Наиболее полезные направления оптимизации: периоды скользящих средних и расстояния защитных приказов.
- При необходимости можно добавить дополнительные фильтры (волатильность, торговые сессии и т. п.) — архитектура стратегии легко расширяется.
Рекомендуемые диапазоны оптимизации
SlowMaLength: 20 – 120 (шаг 5–10).
FastMaLength: 2 – 10 (шаг 1).
StopLossPoints / TakeProfitPoints: 50 – 200 (в зависимости от волатильности).
Такие диапазоны соответствуют исходной логике советника и подходят для адаптации под другие инструменты.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Dual moving average trend confirmation strategy.
/// Uses a slow EMA and a fast LWMA to detect synchronized trends.
/// Enters long when both averages slope upward, price stays above the slow EMA, and the slow EMA is above the fast LWMA.
/// Enters short when both averages slope downward, price stays below the slow EMA, and the slow EMA is below the fast LWMA.
/// Built-in stop-loss and take-profit are defined in instrument points.
/// </summary>
public class DualMaTrendConfirmationStrategy : Strategy
{
private readonly StrategyParam<int> _slowMaLength;
private readonly StrategyParam<int> _fastMaLength;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<DataType> _candleType;
private decimal _previousClose;
private decimal _slowPrevious;
private decimal _slowPrevious2;
private decimal _fastPrevious;
private decimal _fastPrevious2;
private int _historyCount;
/// <summary>
/// Slow EMA period length.
/// </summary>
public int SlowMaLength
{
get => _slowMaLength.Value;
set => _slowMaLength.Value = value;
}
/// <summary>
/// Fast LWMA period length.
/// </summary>
public int FastMaLength
{
get => _fastMaLength.Value;
set => _fastMaLength.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in instrument points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance expressed in instrument points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="DualMaTrendConfirmationStrategy"/> class.
/// </summary>
public DualMaTrendConfirmationStrategy()
{
_slowMaLength = Param(nameof(SlowMaLength), 57)
.SetDisplay("Slow EMA Length", "Period for the slow EMA trend filter", "Moving Averages")
.SetRange(10, 200)
;
_fastMaLength = Param(nameof(FastMaLength), 3)
.SetDisplay("Fast LWMA Length", "Period for the fast LWMA confirmation filter", "Moving Averages")
.SetRange(1, 50)
;
_stopLossPoints = Param(nameof(StopLossPoints), 100m)
.SetDisplay("Stop Loss (points)", "Stop-loss distance measured in instrument points", "Risk Management")
.SetRange(10m, 500m)
;
_takeProfitPoints = Param(nameof(TakeProfitPoints), 100m)
.SetDisplay("Take Profit (points)", "Take-profit distance measured in instrument points", "Risk Management")
.SetRange(10m, 500m)
;
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles used for moving average calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
// Clear stored history so the next candle starts with a clean state.
_previousClose = 0m;
_slowPrevious = 0m;
_slowPrevious2 = 0m;
_fastPrevious = 0m;
_fastPrevious2 = 0m;
_historyCount = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var slowEma = new ExponentialMovingAverage
{
Length = SlowMaLength
};
var fastLwma = new WeightedMovingAverage
{
Length = FastMaLength
};
var subscription = SubscribeCandles(CandleType);
var step = Security.PriceStep ?? 1m;
// Enable automatic stop-loss and take-profit management based on point offsets.
StartProtection(
takeProfit: new Unit(TakeProfitPoints * step, UnitTypes.Absolute),
stopLoss: new Unit(StopLossPoints * step, UnitTypes.Absolute),
useMarketOrders: true);
subscription
.Bind(slowEma, fastLwma, (candle, slowValue, fastValue) => ProcessCandle(candle, slowValue, fastValue, slowEma, fastLwma))
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, slowEma);
DrawIndicator(area, fastLwma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal slowValue, decimal fastValue, ExponentialMovingAverage slowEma, WeightedMovingAverage fastLwma)
{
// Work only with fully formed candles to avoid premature decisions.
if (candle.State != CandleStates.Finished)
return;
// Ensure both indicators produced reliable values before trading logic.
if (!slowEma.IsFormed || !fastLwma.IsFormed)
{
UpdateHistory(slowValue, fastValue, candle.ClosePrice);
return;
}
// Accumulate at least two previous candles for slope calculations.
if (_historyCount < 2)
{
UpdateHistory(slowValue, fastValue, candle.ClosePrice);
return;
}
var slowRising = slowValue > _slowPrevious && _slowPrevious > _slowPrevious2;
var fastRising = fastValue > _fastPrevious && _fastPrevious > _fastPrevious2;
var slowFalling = slowValue < _slowPrevious && _slowPrevious < _slowPrevious2;
var fastFalling = fastValue < _fastPrevious && _fastPrevious < _fastPrevious2;
var priceAboveSlow = _previousClose > _slowPrevious;
var priceBelowSlow = _previousClose < _slowPrevious;
var slowAboveFast = slowValue > fastValue;
var slowBelowFast = slowValue < fastValue;
if (slowRising && fastRising && priceAboveSlow && slowAboveFast && Position <= 0)
{
BuyMarket();
}
else if (slowFalling && fastFalling && priceBelowSlow && slowBelowFast && Position >= 0)
{
SellMarket();
}
UpdateHistory(slowValue, fastValue, candle.ClosePrice);
}
private void UpdateHistory(decimal slowValue, decimal fastValue, decimal closePrice)
{
// Shift previous values so the last two candles are always available.
_slowPrevious2 = _slowPrevious;
_slowPrevious = slowValue;
_fastPrevious2 = _fastPrevious;
_fastPrevious = fastValue;
_previousClose = closePrice;
if (_historyCount < 2)
_historyCount++;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage, WeightedMovingAverage
from StockSharp.Algo.Strategies import Strategy
class dual_ma_trend_confirmation_strategy(Strategy):
def __init__(self):
super(dual_ma_trend_confirmation_strategy, self).__init__()
self._slow_ma_length = self.Param("SlowMaLength", 57)
self._fast_ma_length = self.Param("FastMaLength", 3)
self._stop_loss_points = self.Param("StopLossPoints", 100.0)
self._take_profit_points = self.Param("TakeProfitPoints", 100.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._previous_close = 0.0
self._slow_previous = 0.0
self._slow_previous2 = 0.0
self._fast_previous = 0.0
self._fast_previous2 = 0.0
self._history_count = 0
@property
def SlowMaLength(self):
return self._slow_ma_length.Value
@SlowMaLength.setter
def SlowMaLength(self, value):
self._slow_ma_length.Value = value
@property
def FastMaLength(self):
return self._fast_ma_length.Value
@FastMaLength.setter
def FastMaLength(self, value):
self._fast_ma_length.Value = value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stop_loss_points.Value = value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@TakeProfitPoints.setter
def TakeProfitPoints(self, value):
self._take_profit_points.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(dual_ma_trend_confirmation_strategy, self).OnStarted2(time)
self._previous_close = 0.0
self._slow_previous = 0.0
self._slow_previous2 = 0.0
self._fast_previous = 0.0
self._fast_previous2 = 0.0
self._history_count = 0
self._slow_ema = ExponentialMovingAverage()
self._slow_ema.Length = self.SlowMaLength
self._fast_lwma = WeightedMovingAverage()
self._fast_lwma.Length = self.FastMaLength
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if step <= 0.0:
step = 1.0
self.StartProtection(
Unit(float(self.TakeProfitPoints) * step, UnitTypes.Absolute),
Unit(float(self.StopLossPoints) * step, UnitTypes.Absolute),
False, None, None, True)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._slow_ema, self._fast_lwma, self.ProcessCandle).Start()
def ProcessCandle(self, candle, slow_value, fast_value):
if candle.State != CandleStates.Finished:
return
sv = float(slow_value)
fv = float(fast_value)
close = float(candle.ClosePrice)
if not self._slow_ema.IsFormed or not self._fast_lwma.IsFormed:
self._update_history(sv, fv, close)
return
if self._history_count < 2:
self._update_history(sv, fv, close)
return
slow_rising = sv > self._slow_previous and self._slow_previous > self._slow_previous2
fast_rising = fv > self._fast_previous and self._fast_previous > self._fast_previous2
slow_falling = sv < self._slow_previous and self._slow_previous < self._slow_previous2
fast_falling = fv < self._fast_previous and self._fast_previous < self._fast_previous2
price_above_slow = self._previous_close > self._slow_previous
price_below_slow = self._previous_close < self._slow_previous
slow_above_fast = sv > fv
slow_below_fast = sv < fv
if slow_rising and fast_rising and price_above_slow and slow_above_fast and self.Position <= 0:
self.BuyMarket()
elif slow_falling and fast_falling and price_below_slow and slow_below_fast and self.Position >= 0:
self.SellMarket()
self._update_history(sv, fv, close)
def _update_history(self, slow_value, fast_value, close):
self._slow_previous2 = self._slow_previous
self._slow_previous = slow_value
self._fast_previous2 = self._fast_previous
self._fast_previous = fast_value
self._previous_close = close
if self._history_count < 2:
self._history_count += 1
def OnReseted(self):
super(dual_ma_trend_confirmation_strategy, self).OnReseted()
self._previous_close = 0.0
self._slow_previous = 0.0
self._slow_previous2 = 0.0
self._fast_previous = 0.0
self._fast_previous2 = 0.0
self._history_count = 0
def CreateClone(self):
return dual_ma_trend_confirmation_strategy()