Стратегия возврата к VWAP
Стратегия торгует на откатах от цены, взвешенной по объёму (VWAP). ATR используется, чтобы определить, насколько цена должна отклониться от VWAP, прежде чем рассматривать сделку на разворот.
Тестирование показывает среднегодичную доходность около 58%. Стратегию лучше запускать на фондовом рынке.
Длинная позиция открывается, когда цена падает ниже VWAP более чем на K значений ATR. Короткая позиция берётся, когда цена поднимается выше VWAP на ту же величину. Сделки закрываются, как только цена возвращается к линии VWAP.
Подход рассчитан на внутридневных трейдеров, ожидающих колебаний цены вокруг VWAP, а не сильных трендов. Стопы, определяемые как кратное ATR, помогают ограничить убытки, если движение продолжится против позиции.
Подробности
- Условия входа:
- Long: закрытие < VWAP − K * ATR
- Short: закрытие > VWAP + K * ATR
- Long/Short: обе стороны.
- Условия выхода:
- Long: выход при закрытии >= VWAP
- Short: выход при закрытии <= VWAP
- Стопы: да, стоп на основе ATR.
- Параметры по умолчанию:
K= 2.0mCandleType= TimeSpan.FromMinutes(5)AtrPeriod= 14
- Фильтры:
- Категория: Возврат к среднему
- Направление: Обе стороны
- Индикаторы: VWAP, ATR
- Стопы: Да
- Сложность: Средняя
- Таймфрейм: Внутридневной
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
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>
/// VWAP Mean Reversion Strategy.
/// Enter when price deviates from VWAP by a certain ATR multiple.
/// Exit when price returns to VWAP.
/// </summary>
public class VwapMeanReversionStrategy : Strategy
{
private readonly StrategyParam<decimal> _kParam;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _atrPeriod;
private AverageTrueRange _atr;
private VolumeWeightedMovingAverage _vwap;
private decimal _currentAtr;
private decimal _currentVwap;
/// <summary>
/// ATR multiplier for entry.
/// </summary>
public decimal K
{
get => _kParam.Value;
set => _kParam.Value = value;
}
/// <summary>
/// Type of candles to use.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// ATR period.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="VwapMeanReversionStrategy"/>.
/// </summary>
public VwapMeanReversionStrategy()
{
_kParam = Param(nameof(K), 2.0m)
.SetGreaterThanZero()
.SetDisplay("ATR Multiplier", "ATR multiplier for entry distance from VWAP", "Strategy Parameters")
.SetOptimize(1.0m, 4.0m, 0.5m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "Strategy Parameters");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "ATR indicator period", "Strategy Parameters")
.SetOptimize(10, 20, 2);
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_atr = null;
_vwap = null;
_currentAtr = default;
_currentVwap = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create indicators
_atr = new AverageTrueRange { Length = AtrPeriod };
_vwap = new VolumeWeightedMovingAverage { Length = AtrPeriod };
// Create subscription for candles
var subscription = SubscribeCandles(CandleType);
// Bind indicators to candles
subscription
.Bind(_atr, ProcessATR)
.Start();
// Setup chart visualization if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _atr);
DrawOwnTrades(area);
}
// Enable position protection
StartProtection(
takeProfit: new Unit(5, UnitTypes.Percent),
stopLoss: new Unit(2, UnitTypes.Percent)
);
}
private void ProcessATR(ICandleMessage candle, decimal atr)
{
if (candle.State != CandleStates.Finished)
return;
try
{
_currentVwap = _vwap.Process(candle).ToDecimal();
}
catch
{
return;
}
_currentAtr = atr;
ProcessStrategy(candle.ClosePrice);
}
private void ProcessStrategy(decimal currentPrice)
{
// Check if strategy is ready for trading
if (!IsFormedAndOnlineAndAllowTrading())
return;
// Skip if we don't have valid VWAP or ATR yet
if (_currentVwap <= 0 || _currentAtr <= 0)
return;
// Calculate distance to VWAP
var upperBand = _currentVwap + K * _currentAtr;
var lowerBand = _currentVwap - K * _currentAtr;
LogInfo($"Current Price: {currentPrice}, VWAP: {_currentVwap}, Upper: {upperBand}, Lower: {lowerBand}");
// Entry logic
if (Position == 0)
{
// Long Entry: Price is below lower band
if (currentPrice < lowerBand)
{
// Buy when price is too low compared to VWAP
LogInfo($"Buy Signal - Price ({currentPrice}) < Lower Band ({lowerBand})");
BuyMarket(Volume);
}
// Short Entry: Price is above upper band
else if (currentPrice > upperBand)
{
// Sell when price is too high compared to VWAP
LogInfo($"Sell Signal - Price ({currentPrice}) > Upper Band ({upperBand})");
SellMarket(Volume);
}
}
// Exit logic
else if (Position > 0 && currentPrice > _currentVwap)
{
// Exit Long: Price returned to VWAP
LogInfo($"Exit Long - Price ({currentPrice}) > VWAP ({_currentVwap})");
SellMarket(Math.Abs(Position));
}
else if (Position < 0 && currentPrice < _currentVwap)
{
// Exit Short: Price returned to VWAP
LogInfo($"Exit Short - Price ({currentPrice}) < VWAP ({_currentVwap})");
BuyMarket(Math.Abs(Position));
}
}
}
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 AverageTrueRange, VolumeWeightedMovingAverage
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class vwap_mean_reversion_strategy(Strategy):
"""
VWAP Mean Reversion Strategy.
Enter when price deviates from VWAP by a certain ATR multiple.
Exit when price returns to VWAP.
"""
def __init__(self):
super(vwap_mean_reversion_strategy, self).__init__()
# Initialize strategy parameters
self._k_param = self.Param("K", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("ATR Multiplier", "ATR multiplier for entry distance from VWAP", "Strategy Parameters") \
.SetCanOptimize(True) \
.SetOptimize(1.0, 4.0, 0.5)
self._candle_type = self.Param("CandleType", tf(5)) \
.SetDisplay("Candle Type", "Type of candles to use", "Strategy Parameters")
self._atr_period = self.Param("AtrPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("ATR Period", "ATR indicator period", "Strategy Parameters") \
.SetCanOptimize(True) \
.SetOptimize(10, 20, 2)
# Internal indicators
self._atr = None
self._vwap = None
self._current_atr = 0
self._current_vwap = 0
@property
def K(self):
"""ATR multiplier for entry."""
return self._k_param.Value
@K.setter
def K(self, value):
self._k_param.Value = value
@property
def CandleType(self):
"""Type of candles to use."""
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def AtrPeriod(self):
"""ATR period."""
return self._atr_period.Value
@AtrPeriod.setter
def AtrPeriod(self, value):
self._atr_period.Value = value
def GetWorkingSecurities(self):
"""!! REQUIRED!! Override to return securities used by the strategy."""
return [(self.Security, self.CandleType)]
def OnStarted2(self, time):
"""Set up indicators, subscriptions and protection."""
super(vwap_mean_reversion_strategy, self).OnStarted2(time)
# Create indicators
self._atr = AverageTrueRange()
self._atr.Length = self.AtrPeriod
self._vwap = VolumeWeightedMovingAverage()
self._vwap.Length = self.AtrPeriod
# Create subscription for candles
subscription = self.SubscribeCandles(self.CandleType)
# Bind indicators to candles
subscription.Bind(self._atr, self.ProcessATR).Start()
# Setup chart visualization if available
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._atr)
self.DrawOwnTrades(area)
# Enable position protection
self.StartProtection(
takeProfit=Unit(5, UnitTypes.Percent),
stopLoss=Unit(2, UnitTypes.Percent)
)
def OnReseted(self):
super(vwap_mean_reversion_strategy, self).OnReseted()
self._current_atr = 0
self._current_vwap = 0
def ProcessATR(self, candle, atr_value):
if candle.State != CandleStates.Finished:
return
self._current_vwap = float(process_candle(self._vwap, candle))
self._current_vwap = self._current_vwap if self._current_vwap is not None else 0
self._current_atr = atr_value
self.ProcessStrategy(candle.ClosePrice)
def ProcessStrategy(self, current_price):
# Check if strategy is ready for trading
# Skip if we don't have valid VWAP or ATR yet
if self._current_vwap <= 0 or self._current_atr <= 0:
return
# Calculate distance to VWAP
upper_band = self._current_vwap + self.K * self._current_atr
lower_band = self._current_vwap - self.K * self._current_atr
self.LogInfo(
"Current Price: {0}, VWAP: {1}, Upper: {2}, Lower: {3}".format(
current_price, self._current_vwap, upper_band, lower_band))
# Entry logic
if self.Position == 0:
# Long Entry: Price is below lower band
if current_price < lower_band:
# Buy when price is too low compared to VWAP
self.LogInfo(
"Buy Signal - Price ({0}) < Lower Band ({1})".format(current_price, lower_band))
self.BuyMarket(self.Volume)
# Short Entry: Price is above upper band
elif current_price > upper_band:
# Sell when price is too high compared to VWAP
self.LogInfo(
"Sell Signal - Price ({0}) > Upper Band ({1})".format(current_price, upper_band))
self.SellMarket(self.Volume)
# Exit logic
elif self.Position > 0 and current_price > self._current_vwap:
# Exit Long: Price returned to VWAP
self.LogInfo(
"Exit Long - Price ({0}) > VWAP ({1})".format(current_price, self._current_vwap))
self.SellMarket(Math.Abs(self.Position))
elif self.Position < 0 and current_price < self._current_vwap:
# Exit Short: Price returned to VWAP
self.LogInfo(
"Exit Short - Price ({0}) < VWAP ({1})".format(current_price, self._current_vwap))
self.BuyMarket(Math.Abs(self.Position))
def CreateClone(self):
"""!! REQUIRED!! Creates a new instance of the strategy."""
return vwap_mean_reversion_strategy()