View on GitHub

VWAP Behavioral Bias Filter

The VWAP Behavioral Bias Filter strategy is built around VWAP Behavioral Bias Filter.

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

Signals trigger when Behavioral confirms filtered entries on intraday (5m) data. This makes the method suitable for active traders.

Stops rely on ATR multiples and factors like BiasThreshold, BiasWindowSize. Adjust these defaults to balance risk and reward.

Details

  • Entry Criteria: see implementation for indicator conditions.
  • Long/Short: Both directions.
  • Exit Criteria: opposite signal or stop logic.
  • Stops: Yes, using indicator-based calculations.
  • Default Values:
    • BiasThreshold = 0.5m
    • BiasWindowSize = 20
    • StopLoss = 2m
    • CandleType = TimeSpan.FromMinutes(5).TimeFrame()
  • Filters:
    • Category: Trend following
    • Direction: Both
    • Indicators: Behavioral, Bias
    • Stops: Yes
    • Complexity: Intermediate
    • Timeframe: Intraday (5m)
    • Seasonality: No
    • Neural Networks: No
    • Divergence: No
    • Risk Level: Medium
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 with Behavioral Bias Filter strategy.
/// Entry condition:
/// Long: Price < VWAP && Bias_Score < -Threshold (oversold with panic)
/// Short: Price > VWAP && Bias_Score > Threshold (overbought with euphoria)
/// Exit condition:
/// Long: Price > VWAP
/// Short: Price < VWAP
/// </summary>
public class VwapWithBehavioralBiasFilterStrategy : Strategy
{
	private static readonly object _biasSync = new();

	private readonly StrategyParam<decimal> _biasThreshold;
	private readonly StrategyParam<int> _biasWindowSize;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<DataType> _candleType;

	private VolumeWeightedMovingAverage _vwap;
	private decimal _currentBiasScore;

	// Tracks recent price movements for bias calculation
	private readonly Queue<decimal> _recentPriceMovements = [];

	// Flags to track positions
	private bool _isLong;
	private bool _isShort;

	/// <summary>
	/// Behavioral bias threshold for entry signal.
	/// </summary>
	public decimal BiasThreshold
	{
		get => _biasThreshold.Value;
		set => _biasThreshold.Value = value;
	}

	/// <summary>
	/// Window size for behavioral bias calculation.
	/// </summary>
	public int BiasWindowSize
	{
		get => _biasWindowSize.Value;
		set => _biasWindowSize.Value = value;
	}

	/// <summary>
	/// Stop loss percentage.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Type of candles to use.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Constructor with default parameters.
	/// </summary>
	public VwapWithBehavioralBiasFilterStrategy()
	{
		_biasThreshold = Param(nameof(BiasThreshold), 0.5m)
		.SetGreaterThanZero()
		.SetDisplay("Bias Threshold", "Threshold for behavioral bias", "Behavioral Settings")
		
		.SetOptimize(0.3m, 0.7m, 0.1m);

		_biasWindowSize = Param(nameof(BiasWindowSize), 20)
		.SetGreaterThanZero()
		.SetDisplay("Bias Window Size", "Window size for behavioral bias calculation", "Behavioral Settings")
		
		.SetOptimize(10, 30, 5);

		_stopLoss = Param(nameof(StopLoss), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Stop Loss (%)", "Stop Loss percentage from entry price", "Risk Management")
		
		.SetOptimize(1m, 3m, 0.5m);

		_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();

		_isLong = false;
		_isShort = false;
		_currentBiasScore = 0;
		_recentPriceMovements.Clear();
		_vwap = default;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		// Initialize VWAP indicator
		_vwap = new();

		// Subscribe to candles and bind indicator
		var subscription = SubscribeCandles(CandleType);

		subscription
		.Bind(_vwap, ProcessCandle)
		.Start();

		// Create chart visualization if available
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _vwap);
			DrawOwnTrades(area);
		}

		// Enable position protection with stop-loss
		StartProtection(
		new Unit(0),  // No take profit
		new Unit(StopLoss, UnitTypes.Percent) // Stop-loss as percentage
		);
	}

	/// <summary>
	/// Process each candle and VWAP value.
	/// </summary>
	private void ProcessCandle(ICandleMessage candle, decimal vwapValue)
	{
		// Skip unfinished candles
		if (candle.State != CandleStates.Finished)
		return;

		// Check if strategy is ready to trade
		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		// Update behavioral bias score
		UpdateBehavioralBias(candle);

		var price = candle.ClosePrice;
		var priceBelowVwap = price < vwapValue;
		var priceAboveVwap = price > vwapValue;

		// Trading logic

		// Entry conditions

		// Long entry: Price below VWAP and negative bias score (panic)
		if (priceBelowVwap && _currentBiasScore < -BiasThreshold && !_isLong && Position <= 0)
		{
			LogInfo($"Long signal: Price {price} < VWAP {vwapValue}, Bias {_currentBiasScore} < -Threshold {-BiasThreshold}");
			BuyMarket(Volume);
			_isLong = true;
			_isShort = false;
		}
		// Short entry: Price above VWAP and positive bias score (euphoria)
		else if (priceAboveVwap && _currentBiasScore > BiasThreshold && !_isShort && Position >= 0)
		{
			LogInfo($"Short signal: Price {price} > VWAP {vwapValue}, Bias {_currentBiasScore} > Threshold {BiasThreshold}");
			SellMarket(Volume);
			_isShort = true;
			_isLong = false;
		}

		// Exit conditions

		// Exit long: Price rises above VWAP
		if (_isLong && priceAboveVwap && Position > 0)
		{
			LogInfo($"Exit long: Price {price} > VWAP {vwapValue}");
			SellMarket(Math.Abs(Position));
			_isLong = false;
		}
		// Exit short: Price falls below VWAP
		else if (_isShort && priceBelowVwap && Position < 0)
		{
			LogInfo($"Exit short: Price {price} < VWAP {vwapValue}");
			BuyMarket(Math.Abs(Position));
			_isShort = false;
		}
	}

	/// <summary>
	/// Update behavioral bias score based on recent price movements.
	/// This is a simplified model of behavioral biases in markets.
	/// </summary>
	private void UpdateBehavioralBias(ICandleMessage candle)
	{
		lock (_biasSync)
		{
			// Calculate price movement %
			decimal priceChange = 0;
			if (candle.OpenPrice != 0)
			{
				priceChange = (candle.ClosePrice - candle.OpenPrice) / candle.OpenPrice * 100;
			}

			// Add to queue
			_recentPriceMovements.Enqueue(priceChange);

			// Maintain window size
			while (_recentPriceMovements.Count > BiasWindowSize)
			{
				_recentPriceMovements.Dequeue();
			}

			// Not enough data yet
			if (_recentPriceMovements.Count < 5)
			{
				_currentBiasScore = 0;
				return;
			}

			var movements = _recentPriceMovements.ToArray();

			// Calculate various components of bias score
			decimal recentMovement = 0;
			for (var i = Math.Max(0, movements.Length - 5); i < movements.Length; i++)
				recentMovement += movements[i];

			decimal sum = 0;
			decimal sumSquared = 0;

			foreach (var movement in movements)
			{
				sum += movement;
				sumSquared += movement * movement;
			}

			var avg = sum / movements.Length;
			var variance = (sumSquared / movements.Length) - (avg * avg);
			var volatility = (decimal)Math.Sqrt((double)Math.Max(0, variance));

			decimal previousMove = 0;
			int consecutiveSameDirection = 0;
			int maxConsecutive = 0;

			foreach (var movement in movements)
			{
				if (previousMove != 0 && Math.Sign(movement) == Math.Sign(previousMove))
				{
					consecutiveSameDirection++;
					maxConsecutive = Math.Max(maxConsecutive, consecutiveSameDirection);
				}
				else
				{
					consecutiveSameDirection = 0;
				}

				previousMove = movement;
			}

			decimal bodySize = Math.Abs(candle.ClosePrice - candle.OpenPrice);
			decimal totalSize = candle.HighPrice - candle.LowPrice;
			decimal bodyRatio = totalSize > 0 ? bodySize / totalSize : 0;

			_currentBiasScore = 0;
			_currentBiasScore += Math.Min(0.5m, Math.Max(-0.5m, recentMovement / 2));
			_currentBiasScore += Math.Sign(recentMovement) * Math.Min(0.3m, volatility / 10);
			_currentBiasScore += Math.Sign(recentMovement) * Math.Min(0.2m, maxConsecutive / 10.0m);

			if (candle.ClosePrice > candle.OpenPrice)
				_currentBiasScore += bodyRatio * 0.2m;
			else
				_currentBiasScore -= bodyRatio * 0.2m;

			_currentBiasScore = Math.Max(-1.0m, Math.Min(1.0m, _currentBiasScore));

			LogInfo($"Behavioral Bias: {_currentBiasScore}, Components: Momentum={recentMovement/2}, Volatility={volatility/10}, Herding={maxConsecutive/10.0m}, Candle={bodyRatio*0.2m}");
		}
	}
}