Ver en GitHub

Estrategia VWAP con Filtro de Sesgo Conductual

La estrategia VWAP Behavioral Bias Filter está construida en torno al filtro de sesgo conductual de VWAP.

Las pruebas indican un retorno anual promedio de aproximadamente 124%. Funciona mejor en el mercado de divisas.

Las señales se activan cuando el filtro Behavioral confirma entradas filtradas en datos intradía (5m). Esto hace que el método sea adecuado para traders activos.

Los stops se basan en múltiplos de ATR y factores como BiasThreshold, BiasWindowSize. Ajuste estos valores predeterminados para equilibrar el riesgo y la recompensa.

Detalles

  • Criterios de entrada: ver implementación para condiciones de indicadores.
  • Largo/Corto: Ambos direcciones.
  • Criterios de salida: señal opuesta o lógica de stop.
  • Stops: Sí, usando cálculos basados en indicadores.
  • Valores predeterminados:
    • BiasThreshold = 0.5m
    • BiasWindowSize = 20
    • StopLoss = 2m
    • CandleType = TimeSpan.FromMinutes(5).TimeFrame()
  • Filtros:
    • Categoría: Seguimiento de tendencia
    • Dirección: Ambos
    • Indicadores: Behavioral, Bias
    • Stops: Sí
    • Complejidad: Intermedio
    • Marco temporal: Intradía (5m)
    • Estacionalidad: No
    • Redes neuronales: No
    • Divergencia: No
    • Nivel de riesgo: Medio
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}");
		}
	}
}