VWAP и стохастическая дивергенция
Стратегия VWAP Stochastic Divergence комбинирует VWAP с индикатором силы тренда ADX. Сигналы формируются, когда Stochastic подтверждает дивергенцию на внутридневных данных (5м). Такой подход подходит активным трейдерам. Стопы рассчитываются исходя из кратных ATR и параметров AdxPeriod, AdxThreshold. Эти значения можно изменять для баланса риска и прибыли.
Тестирование показывает среднегодичную доходность около 79%. Стратегию лучше запускать на фондовом рынке.
Подробности
- Условия входа: см. реализацию для условий по индикаторам.
- Длинные/короткие позиции: обе стороны.
- Условия выхода: обратный сигнал или логика стопов.
- Стопы: да, вычисляются на основе индикаторов.
- Значения по умолчанию:
AdxPeriod = 14AdxThreshold = 25mAdxExitThreshold = 20mCandleType = TimeSpan.FromMinutes(5).TimeFrame()
- Фильтры:
- Категория: Следование за трендом
- Направление: Оба
- Индикаторы: Stochastic, Divergence
- Стопы: Да
- Сложность: Средняя
- Таймфрейм: Внутридневной (5m)
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Да
- Уровень риска: Средний
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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy combining VWAP with ADX trend strength indicator.
/// </summary>
public class VwapAdxTrendStrategy : Strategy
{
private readonly StrategyParam<int> _adxPeriod;
private readonly StrategyParam<decimal> _adxThreshold;
private readonly StrategyParam<decimal> _adxExitThreshold;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _signalCooldownBars;
private VolumeWeightedMovingAverage _vwap;
private AverageDirectionalIndex _adx;
private DirectionalIndex _di;
private decimal _vwapValue;
private decimal _adxValue;
private decimal _plusDiValue;
private decimal _minusDiValue;
private decimal? _prevPlusDiValue;
private decimal? _prevMinusDiValue;
private int _cooldownRemaining;
/// <summary>
/// ADX period for trend strength calculation.
/// </summary>
public int AdxPeriod
{
get => _adxPeriod.Value;
set => _adxPeriod.Value = value;
}
/// <summary>
/// ADX threshold for trend strength entry.
/// </summary>
public decimal AdxThreshold
{
get => _adxThreshold.Value;
set => _adxThreshold.Value = value;
}
/// <summary>
/// ADX threshold for trend strength exit.
/// </summary>
public decimal AdxExitThreshold
{
get => _adxExitThreshold.Value;
set => _adxExitThreshold.Value = value;
}
/// <summary>
/// Candle type to use for the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Number of closed candles to wait before entering a new trend signal.
/// </summary>
public int SignalCooldownBars
{
get => _signalCooldownBars.Value;
set => _signalCooldownBars.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="VwapAdxTrendStrategy"/>.
/// </summary>
public VwapAdxTrendStrategy()
{
_adxPeriod = Param(nameof(AdxPeriod), 14)
.SetDisplay("ADX Period", "Period for ADX and Directional Index calculations", "ADX")
.SetOptimize(8, 20, 2);
_adxThreshold = Param(nameof(AdxThreshold), 25m)
.SetDisplay("ADX Threshold", "ADX threshold for trend strength entry", "ADX")
.SetOptimize(20m, 40m, 5m);
_adxExitThreshold = Param(nameof(AdxExitThreshold), 20m)
.SetDisplay("ADX Exit Threshold", "ADX threshold for trend strength exit", "ADX")
.SetOptimize(10m, 25m, 5m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 4)
.SetNotNegative()
.SetDisplay("Signal Cooldown Bars", "Closed candles to wait before a new DI crossover entry", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_vwapValue = default;
_adxValue = default;
_plusDiValue = default;
_minusDiValue = default;
_prevPlusDiValue = null;
_prevMinusDiValue = null;
_cooldownRemaining = 0;
_vwap = null;
_adx = null;
_di = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create indicators
_vwap = new VolumeWeightedMovingAverage();
_adx = new AverageDirectionalIndex
{
Length = AdxPeriod
};
_di = new DirectionalIndex
{
Length = AdxPeriod
};
// Create subscription and bind indicators
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(
_vwap,
_adx,
_di,
ProcessCandle)
.Start();
// Setup chart visualization if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _vwap);
DrawIndicator(area, _adx);
DrawOwnTrades(area);
}
// Setup position protection
StartProtection(
new Unit(2, UnitTypes.Percent),
new Unit(2, UnitTypes.Percent)
);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue vwapValue, IIndicatorValue adxValue, IIndicatorValue diValue)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var adxTyped = (AverageDirectionalIndexValue)adxValue;
if (adxTyped.MovingAverage is not decimal adx)
return;
var dx = adxTyped.Dx;
if (dx.Plus is not decimal plusDi || dx.Minus is not decimal minusDi)
return;
// Extract values from indicators
_vwapValue = vwapValue.ToDecimal();
_adxValue = adx;
_plusDiValue = plusDi; // +DI
_minusDiValue = minusDi; // -DI
if (_prevPlusDiValue is null || _prevMinusDiValue is null)
{
_prevPlusDiValue = _plusDiValue;
_prevMinusDiValue = _minusDiValue;
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
return;
var bullishCross = _prevPlusDiValue.Value <= _prevMinusDiValue.Value && _plusDiValue > _minusDiValue;
var bearishCross = _prevPlusDiValue.Value >= _prevMinusDiValue.Value && _minusDiValue > _plusDiValue;
// Trading logic
if (_cooldownRemaining == 0 && bullishCross && candle.ClosePrice > _vwapValue && _adxValue > AdxThreshold && Position <= 0)
{
BuyMarket(Volume + (Position < 0 ? Math.Abs(Position) : 0m));
_cooldownRemaining = SignalCooldownBars;
}
else if (_cooldownRemaining == 0 && bearishCross && candle.ClosePrice < _vwapValue && _adxValue > AdxThreshold && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_cooldownRemaining = SignalCooldownBars;
}
// Exit long position when ADX weakens below exit threshold or -DI crosses above +DI
else if (Position > 0 && (_adxValue < AdxExitThreshold || _minusDiValue > _plusDiValue))
{
SellMarket(Position);
_cooldownRemaining = SignalCooldownBars;
}
// Exit short position when ADX weakens below exit threshold or +DI crosses above -DI
else if (Position < 0 && (_adxValue < AdxExitThreshold || _plusDiValue > _minusDiValue))
{
BuyMarket(Math.Abs(Position));
_cooldownRemaining = SignalCooldownBars;
}
_prevPlusDiValue = _plusDiValue;
_prevMinusDiValue = _minusDiValue;
}
}
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, Unit, UnitTypes, CandleStates
from StockSharp.Algo.Indicators import VolumeWeightedMovingAverage, AverageDirectionalIndex, DirectionalIndex
from StockSharp.Algo.Strategies import Strategy
class vwap_adx_trend_strategy(Strategy):
"""
Strategy combining VWAP with ADX trend strength indicator.
"""
def __init__(self):
super(vwap_adx_trend_strategy, self).__init__()
self._adx_period = self.Param("AdxPeriod", 14) \
.SetDisplay("ADX Period", "Period for ADX and Directional Index calculations", "ADX") \
.SetCanOptimize(True) \
.SetOptimize(8, 20, 2)
self._adx_threshold = self.Param("AdxThreshold", 25.0) \
.SetDisplay("ADX Threshold", "ADX threshold for trend strength entry", "ADX") \
.SetCanOptimize(True) \
.SetOptimize(20.0, 40.0, 5.0)
self._adx_exit_threshold = self.Param("AdxExitThreshold", 20.0) \
.SetDisplay("ADX Exit Threshold", "ADX threshold for trend strength exit", "ADX") \
.SetCanOptimize(True) \
.SetOptimize(10.0, 25.0, 5.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 4) \
.SetNotNegative() \
.SetDisplay("Signal Cooldown Bars", "Closed candles to wait before a new DI crossover entry", "General")
self._vwap_value = 0.0
self._adx_value = 0.0
self._plus_di_value = 0.0
self._minus_di_value = 0.0
self._prev_plus_di = None
self._prev_minus_di = None
self._cooldown_remaining = 0
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type)]
def OnReseted(self):
super(vwap_adx_trend_strategy, self).OnReseted()
self._vwap_value = 0.0
self._adx_value = 0.0
self._plus_di_value = 0.0
self._minus_di_value = 0.0
self._prev_plus_di = None
self._prev_minus_di = None
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(vwap_adx_trend_strategy, self).OnStarted2(time)
vwap = VolumeWeightedMovingAverage()
adx = AverageDirectionalIndex()
adx.Length = int(self._adx_period.Value)
di = DirectionalIndex()
di.Length = int(self._adx_period.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(vwap, adx, di, self.ProcessCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, vwap)
self.DrawIndicator(area, adx)
self.DrawOwnTrades(area)
self.StartProtection(
Unit(2, UnitTypes.Percent),
Unit(2, UnitTypes.Percent)
)
def ProcessCandle(self, candle, vwap_value, adx_value, di_value):
if candle.State != CandleStates.Finished:
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
if adx_value.MovingAverage is None:
return
adx = float(adx_value.MovingAverage)
dx = adx_value.Dx
if dx.Plus is None or dx.Minus is None:
return
plus_di = float(dx.Plus)
minus_di = float(dx.Minus)
self._vwap_value = float(vwap_value)
self._adx_value = adx
self._plus_di_value = plus_di
self._minus_di_value = minus_di
if self._prev_plus_di is None or self._prev_minus_di is None:
self._prev_plus_di = self._plus_di_value
self._prev_minus_di = self._minus_di_value
return
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_plus_di = self._plus_di_value
self._prev_minus_di = self._minus_di_value
return
bullish_cross = self._prev_plus_di <= self._prev_minus_di and self._plus_di_value > self._minus_di_value
bearish_cross = self._prev_plus_di >= self._prev_minus_di and self._minus_di_value > self._plus_di_value
close_price = float(candle.ClosePrice)
adx_threshold = float(self._adx_threshold.Value)
adx_exit_threshold = float(self._adx_exit_threshold.Value)
cooldown_bars = int(self._signal_cooldown_bars.Value)
if self._cooldown_remaining == 0 and bullish_cross and close_price > self._vwap_value and self._adx_value > adx_threshold and self.Position <= 0:
vol = self.Volume
if self.Position < 0:
vol = self.Volume + Math.Abs(self.Position)
self.BuyMarket(vol)
self._cooldown_remaining = cooldown_bars
elif self._cooldown_remaining == 0 and bearish_cross and close_price < self._vwap_value and self._adx_value > adx_threshold and self.Position >= 0:
self.SellMarket(self.Volume + Math.Abs(self.Position))
self._cooldown_remaining = cooldown_bars
elif self.Position > 0 and (self._adx_value < adx_exit_threshold or self._minus_di_value > self._plus_di_value):
self.SellMarket(self.Position)
self._cooldown_remaining = cooldown_bars
elif self.Position < 0 and (self._adx_value < adx_exit_threshold or self._plus_di_value > self._minus_di_value):
self.BuyMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown_bars
self._prev_plus_di = self._plus_di_value
self._prev_minus_di = self._minus_di_value
def CreateClone(self):
return vwap_adx_trend_strategy()