View on GitHub

CCI VWAP Strategy

The CCI VWAP approach attempts to capture intraday reversals when momentum and price align away from the volume weighted average price. By observing the Commodity Channel Index alongside the VWAP level, the system measures the strength of recent swings relative to a fair value benchmark.

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

A buy setup emerges when the CCI falls below CciOversold and the market trades beneath VWAP, signalling that selling pressure may be exhausted. A short occurs when the CCI rises above CciOverbought with price above VWAP, highlighting a stretched rally vulnerable to a setback. Positions are closed once price reclaims the VWAP in the opposite direction.

This strategy is designed for day traders who like to fade extremes yet still rely on objective levels for exits. The defined stop-loss helps manage risk if momentum does not quickly mean revert.

Details

  • Entry Criteria:
    • Long: CCI < CciOversold && Price < VWAP (oversold below VWAP)
    • Short: CCI > CciOverbought && Price > VWAP (overbought above VWAP)
  • Long/Short: Both sides.
  • Exit Criteria:
    • Long: Exit long when price moves above VWAP
    • Short: Exit short when price falls below VWAP
  • Stops: Yes.
  • Default Values:
    • CciPeriod = 20
    • CciOversold = -100
    • CciOverbought = 100
    • StopLossPercent = 2 -100 and 100 are the defaults of the CCI levels the rules quote. The market trades around the clock, so the session VWAP restarts each UTC day and weighs the typical price of each candle by its volume. An entry signal against an open position reverses it.
    • CandleType = TimeSpan.FromMinutes(5)
  • Filters:
    • Category: Mixed
    • Direction: Both
    • Indicators: CCI VWAP
    • Stops: Yes
    • Complexity: Intermediate
    • 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>
/// CCI VWAP strategy.
/// The market trades around the clock, so the session VWAP restarts with each UTC day and weighs each candle's typical price by its volume.
/// CCI below CciOversold with a close below VWAP goes long and CCI above CciOverbought with a close above VWAP goes short,
/// reversing an opposite position. A long closes once price closes above VWAP and a short once it closes below, and a percent stop limits the loss.
/// </summary>
public class CciVwapStrategy : Strategy
{
	private readonly StrategyParam<int> _cciPeriod;
	private readonly StrategyParam<decimal> _cciOversold;
	private readonly StrategyParam<decimal> _cciOverbought;
	private readonly StrategyParam<decimal> _stopLossPercent;
	private readonly StrategyParam<DataType> _candleType;

	private DateTime? _day;
	private decimal _cumulativePriceVolume;
	private decimal _cumulativeVolume;

	/// <summary>
	/// Period of CCI.
	/// </summary>
	public int CciPeriod
	{
		get => _cciPeriod.Value;
		set => _cciPeriod.Value = value;
	}

	/// <summary>
	/// CCI level for longs.
	/// </summary>
	public decimal CciOversold
	{
		get => _cciOversold.Value;
		set => _cciOversold.Value = value;
	}

	/// <summary>
	/// CCI level for shorts.
	/// </summary>
	public decimal CciOverbought
	{
		get => _cciOverbought.Value;
		set => _cciOverbought.Value = value;
	}

	/// <summary>
	/// Stop loss percentage from entry price.
	/// </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 CciVwapStrategy()
	{
		_cciPeriod = Param(nameof(CciPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("CCI Period", "Period of CCI", "Indicators");

		_cciOversold = Param(nameof(CciOversold), -100m)
			.SetDisplay("CCI Oversold", "CCI level for longs", "Indicators");

		_cciOverbought = Param(nameof(CciOverbought), 100m)
			.SetDisplay("CCI Overbought", "CCI level for shorts", "Indicators");

		_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;
		_cumulativePriceVolume = 0;
		_cumulativeVolume = 0;
	}

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

		_day = null;
		_cumulativePriceVolume = 0;
		_cumulativeVolume = 0;

		var cci = new CommodityChannelIndex { Length = CciPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(cci, 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);

			var oscillators = CreateChartArea();
			if (oscillators != null)
			{
				DrawIndicator(oscillators, cci);
			}
		}
	}

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

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

		var day = candle.OpenTime.Date;

		if (_day != day)
		{
			_day = day;
			_cumulativePriceVolume = 0;
			_cumulativeVolume = 0;
		}

		var typicalPrice = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3;
		_cumulativePriceVolume += typicalPrice * candle.TotalVolume;
		_cumulativeVolume += candle.TotalVolume;

		if (!cciValue.IsFormed || _cumulativeVolume <= 0)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var cci = cciValue.GetValue<decimal>();
		var vwap = _cumulativePriceVolume / _cumulativeVolume;
		var close = candle.ClosePrice;

		if (cci < CciOversold && close < vwap && Position <= 0)
			BuyMarket(Volume + Math.Abs(Position));
		else if (cci > CciOverbought && close > vwap && Position >= 0)
			SellMarket(Volume + Math.Abs(Position));
		else if (Position > 0 && close > vwap)
			SellMarket(Position);
		else if (Position < 0 && close < vwap)
			BuyMarket(-Position);
	}
}