Открыть на GitHub

Стратегия отскока от VWAP

VWAP (объёмная средняя цена) часто используется как внутридневной ориентир. Когда цена значительно отклоняется от VWAP и затем возвращается к нему, часто следует короткое движение в противоположную сторону. Данная стратегия торгует такие отскоки.

Тестирование показывает среднегодичную доходность около 130%. Стратегию лучше запускать на фондовом рынке.

Для каждой свечи вычисляется текущий VWAP. Если бычья свеча закрывается ниже VWAP, открывается длинная позиция; если медвежья свеча закрывается выше VWAP, открывается короткая. Риск управляется фиксированным стоп‑процентом, позиции обычно удерживаются лишь до появления обратного сигнала или срабатывания стопа.

Так как стратегия торгует против экстремумов дня, она лучше всего работает во флэтовом рынке, а не при сильных трендах.

Детали

  • Условия входа: закрытие ниже VWAP с бычьей свечой или выше VWAP с медвежьей свечой.
  • Длинные/короткие: обе стороны.
  • Условия выхода: противоположный сигнал или стоп‑лосс.
  • Стопы: да, процентные.
  • Значения по умолчанию:
    • CandleType = 5 минут
    • StopLoss = 2%
  • Фильтры:
    • Категория: возврат к среднему
    • Направление: оба
    • Индикаторы: VWAP
    • Стопы: да
    • Сложность: базовая
    • Таймфрейм: внутридневной
    • Сезонность: нет
    • Нейросети: нет
    • Дивергенция: нет
    • Уровень риска: средний
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// VWAP Bounce strategy.
/// Enters long when price bounces off VWAP from below with a bullish candle.
/// Enters short when price bounces off VWAP from above with a bearish candle.
/// Uses SMA for exit signals.
/// </summary>
public class VwapBounceStrategy : Strategy
{
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _cooldownBars;

	private decimal _prevClose;
	private int _cooldown;

	/// <summary>
	/// MA Period.
	/// </summary>
	public int MAPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Candle type.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Cooldown bars.
	/// </summary>
	public int CooldownBars
	{
		get => _cooldownBars.Value;
		set => _cooldownBars.Value = value;
	}

	/// <summary>
	/// Constructor.
	/// </summary>
	public VwapBounceStrategy()
	{
		_maPeriod = Param(nameof(MAPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Period for SMA", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to use", "General");

		_cooldownBars = Param(nameof(CooldownBars), 500)
			.SetRange(1, 1000)
			.SetDisplay("Cooldown Bars", "Bars to wait between trades", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevClose = default;
		_cooldown = default;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_prevClose = 0;
		_cooldown = 0;

		var vwma = new VolumeWeightedMovingAverage { Length = 20 };
		var sma = new SimpleMovingAverage { Length = MAPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(vwma, sma, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, vwma);
			DrawIndicator(area, sma);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal vwmaValue, decimal smaValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (_prevClose == 0)
		{
			_prevClose = candle.ClosePrice;
			return;
		}

		if (_cooldown > 0)
		{
			_cooldown--;
			_prevClose = candle.ClosePrice;
			return;
		}

		var isBullish = candle.ClosePrice > candle.OpenPrice;
		var isBearish = candle.ClosePrice < candle.OpenPrice;

		// Bounce off VWAP from below (bullish): prev close was below VWAP, now above or near, bullish candle
		var bouncedUp = _prevClose < vwmaValue && candle.ClosePrice >= vwmaValue && isBullish;
		// Bounce off VWAP from above (bearish): prev close was above VWAP, now below or near, bearish candle
		var bouncedDown = _prevClose > vwmaValue && candle.ClosePrice <= vwmaValue && isBearish;

		if (Position == 0 && bouncedUp)
		{
			BuyMarket();
			_cooldown = CooldownBars;
		}
		else if (Position == 0 && bouncedDown)
		{
			SellMarket();
			_cooldown = CooldownBars;
		}
		else if (Position > 0 && candle.ClosePrice < smaValue)
		{
			SellMarket();
			_cooldown = CooldownBars;
		}
		else if (Position < 0 && candle.ClosePrice > smaValue)
		{
			BuyMarket();
			_cooldown = CooldownBars;
		}

		_prevClose = candle.ClosePrice;
	}
}