View on GitHub

VWAP Bounce Strategy

Volume Weighted Average Price (VWAP) is a popular intraday benchmark. When price deviates significantly from VWAP and then prints a candle back toward it, a brief reversion move often follows. This strategy trades those bounces.

Testing indicates an average annual return of about 130%. It performs best in the stocks market.

For each bar the current VWAP is computed. If a bullish candle closes below VWAP the system goes long; if a bearish candle closes above VWAP it goes short. A fixed stop-loss percentage manages risk, and positions are typically held only until an opposite signal forms or the stop is reached.

Because it fades intraday extremes, the method works best in range‑bound markets rather than strong trends.

Details

  • Entry Criteria: Close below VWAP with bullish candle or above VWAP with bearish candle.
  • Long/Short: Both.
  • Exit Criteria: Opposite signal or stop-loss.
  • Stops: Yes, percentage based.
  • Default Values:
    • CandleType = 5 minute
    • StopLossPercent = 2 The VWAP restarts at the beginning of every UTC day and weights each candle's typical price by its volume.
  • Filters:
    • Category: Mean Reversion
    • Direction: Both
    • Indicators: VWAP
    • Stops: Yes
    • Complexity: Basic
    • Timeframe: Intraday
    • Seasonality: No
    • Neural networks: No
    • Divergence: No
    • Risk level: Medium
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.
/// The VWAP restarts every UTC day from the typical price of each candle weighted by its volume. A bullish candle closing below
/// the VWAP turns the position long and a bearish candle closing above it turns it short; a percent stop limits the loss.
/// </summary>
public class VwapBounceStrategy : Strategy
{
	private readonly StrategyParam<decimal> _stopLossPercent;
	private readonly StrategyParam<DataType> _candleType;

	private DateTime? _day;
	private decimal _priceVolume;
	private decimal _volume;

	/// <summary>
	/// Stop-loss percentage.
	/// </summary>
	public decimal StopLossPercent
	{
		get => _stopLossPercent.Value;
		set => _stopLossPercent.Value = value;
	}

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

	/// <summary>
	/// Constructor.
	/// </summary>
	public VwapBounceStrategy()
	{
		_stopLossPercent = Param(nameof(StopLossPercent), 2m)
			.SetNotNegative()
			.SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk");

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

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_day = null;
		_priceVolume = default;
		_volume = default;
	}

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

		_day = null;
		_priceVolume = default;
		_volume = default;

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

		StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);

		// The stop has to see prices between candles, not only at their close.
		foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
		{
			var quotes = new Subscription(DataType.Level1, Security);
			quotes.MarketData.BuildField = field;
			SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
		}

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

	private void ObserveProtectionQuote(Level1ChangeMessage quote)
	{
		// The high-level handler activates native protection before this callback, also between signal bars.
	}

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

		var day = candle.OpenTime.Date;

		if (_day != day)
		{
			_day = day;
			_priceVolume = 0;
			_volume = 0;
		}

		var typical = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m;
		_priceVolume += typical * candle.TotalVolume;
		_volume += candle.TotalVolume;

		if (_volume <= 0 || !IsFormedAndOnlineAndAllowTrading())
			return;

		var vwap = _priceVolume / _volume;
		var close = candle.ClosePrice;

		if (close > candle.OpenPrice && close < vwap && Position <= 0)
			BuyMarket(Volume + Math.Abs(Position));
		else if (close < candle.OpenPrice && close > vwap && Position >= 0)
			SellMarket(Volume + Math.Abs(Position));
	}
}