Ver en GitHub

Estrategia VLT Trader con Filtro

Descripción general

La Estrategia VLT Trader con Filtro es un sistema de ruptura por contracción de volatilidad convertido desde la implementación MQL original. Monitorea los rangos de candles recientes y prepara órdenes stop siempre que el candle completado más reciente se convierta en el menor rango dentro de una ventana histórica configurable. El objetivo es capturar movimientos explosivos después de un período de consolidación estrecha.

Lógica de trading

  1. Procesamiento de nueva barra – la estrategia evalúa las condiciones solo una vez por nuevo candle. El candle actual debe abrir por debajo del máximo del candle anterior para evitar operar gaps que saltan a través del nivel de ruptura.
  2. Filtro de volatilidad – el rango del candle finalizado más reciente se compara con el menor rango entre los últimos CandleCount candles finalizados cuyo rango está por debajo de MaxCandleSizePips. Si el candle más reciente es estrictamente más pequeño, la configuración es válida.
  3. Colocación de entradas – cuando la configuración es válida, se preparan dos órdenes stop:
    • Un buy stop 10 pips por encima del máximo anterior cuando la posición neta no es larga.
    • Un sell stop 10 pips por debajo del mínimo anterior cuando la posición neta no es corta. Las órdenes pendientes existentes del mismo tipo se cancelan antes de registrar nuevas.
  4. Gestión del riesgo – una vez que una orden stop se activa y abre una posición, se adjuntan automáticamente órdenes de protección:
    • Take-profit en TakeProfitPips por encima/debajo del precio de entrada.
    • Stop-loss en StopLossPips por debajo/encima del precio de entrada. Las órdenes de protección se cancelan cuando la posición vuelve a cero.

Parámetros

Parámetro Descripción
Volume Volumen de la orden enviado con cada orden stop.
TakeProfitPips Distancia en pips usada para la orden take-profit después de la entrada.
StopLossPips Distancia en pips usada para el stop de protección después de la entrada.
MaxCandleSizePips Límite superior para los rangos de candles históricos considerados en el filtro de volatilidad.
CandleCount Número de candles históricos usados para encontrar el rango mínimo aceptable.
CandleType Marco temporal de candles usado para el análisis.

Notas de implementación

  • El tamaño de pip se deriva del paso de precio del instrumento. Cuando el paso es menor o igual a 0.001, se multiplica por 10 para emular la definición de pip de MetaTrader para instrumentos de 3 o 5 decimales.
  • Los rangos de candles se almacenan en una cola FIFO limitada a CandleCount elementos, coincidiendo con el escaneo histórico realizado en el Expert Advisor original.
  • Todas las órdenes se crean a través de la API de alto nivel de StockSharp (sin registro manual de órdenes) y se cancelan automáticamente cuando están obsoletas o cuando la posición se cierra.
  • Los comentarios dentro del código están escritos en inglés, mientras que los archivos README proporcionan documentación multilingüe detallada.
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>
/// Volatility contraction breakout strategy converted from the VLT_TRADER MQL version.
/// Enters when the latest candle range is the smallest within recent history and
/// price breaks above/below the previous candle high/low.
/// </summary>
public class VltTraderFilterStrategy : Strategy
{
	private readonly StrategyParam<int> _candleCount;
	private readonly StrategyParam<decimal> _takeProfitMultiplier;
	private readonly StrategyParam<decimal> _stopLossMultiplier;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _rangeHistory = new();
	private decimal? _prevHigh;
	private decimal? _prevLow;
	private decimal? _prevRange;
	private decimal _entryPrice;
	private bool _isLong;

	/// <summary>
	/// Number of historical candles used for the volatility filter.
	/// </summary>
	public int CandleCount
	{
		get => _candleCount.Value;
		set => _candleCount.Value = value;
	}

	/// <summary>
	/// Take profit as a multiplier of the narrow range candle.
	/// </summary>
	public decimal TakeProfitMultiplier
	{
		get => _takeProfitMultiplier.Value;
		set => _takeProfitMultiplier.Value = value;
	}

	/// <summary>
	/// Stop loss as a multiplier of the narrow range candle.
	/// </summary>
	public decimal StopLossMultiplier
	{
		get => _stopLossMultiplier.Value;
		set => _stopLossMultiplier.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="VltTraderFilterStrategy"/> class.
	/// </summary>
	public VltTraderFilterStrategy()
	{
		_candleCount = Param(nameof(CandleCount), 6)
			.SetGreaterThanZero()
			.SetDisplay("Candle Count", "Number of historical candles used for the volatility filter", "Signals")
			.SetOptimize(3, 15, 1);

		_takeProfitMultiplier = Param(nameof(TakeProfitMultiplier), 3m)
			.SetGreaterThanZero()
			.SetDisplay("TP Multiplier", "Take profit as multiplier of narrow range", "Risk")
			.SetOptimize(1m, 5m, 0.5m);

		_stopLossMultiplier = Param(nameof(StopLossMultiplier), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("SL Multiplier", "Stop loss as multiplier of narrow range", "Risk")
			.SetOptimize(0.5m, 3m, 0.5m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Time frame used to build signal candles", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_rangeHistory.Clear();
		_prevHigh = null;
		_prevLow = null;
		_prevRange = null;
		_entryPrice = 0m;
		_isLong = false;
	}

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

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

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

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

		var high = candle.HighPrice;
		var low = candle.LowPrice;
		var close = candle.ClosePrice;
		var range = high - low;

		// no indicators bound via .Bind()

		// Check exit conditions for existing position
		if (Position != 0 && _entryPrice != 0 && _prevRange is decimal narrowRange && narrowRange > 0)
		{
			var tp = narrowRange * TakeProfitMultiplier;
			var sl = narrowRange * StopLossMultiplier;

			if (_isLong && Position > 0)
			{
				if (close >= _entryPrice + tp || close <= _entryPrice - sl)
				{
					SellMarket();
					UpdateHistory(range, high, low);
					return;
				}
			}
			else if (!_isLong && Position < 0)
			{
				if (close <= _entryPrice - tp || close >= _entryPrice + sl)
				{
					BuyMarket();
					UpdateHistory(range, high, low);
					return;
				}
			}
		}

		// Check entry conditions only when flat
		if (Position == 0 && _prevHigh.HasValue && _prevLow.HasValue && _prevRange.HasValue)
		{
			var prevH = _prevHigh.Value;
			var prevL = _prevLow.Value;
			var prevR = _prevRange.Value;

			if (prevR > 0 && _rangeHistory.Count >= CandleCount)
			{
				// Check if previous candle range was the narrowest
				var isNarrowest = true;
				foreach (var histRange in _rangeHistory)
				{
					if (histRange > 0 && histRange <= prevR)
					{
						isNarrowest = false;
						break;
					}
				}

				if (isNarrowest)
				{
					// Breakout detection on current candle
					if (close > prevH)
					{
						var volume = Volume;
						if (volume > 0)
						{
							BuyMarket();
							_entryPrice = close;
							_isLong = true;
						}
					}
					else if (close < prevL)
					{
						var volume = Volume;
						if (volume > 0)
						{
							SellMarket();
							_entryPrice = close;
							_isLong = false;
						}
					}
				}
			}
		}

		UpdateHistory(range, high, low);
	}

	private void UpdateHistory(decimal range, decimal high, decimal low)
	{
		if (_prevRange.HasValue)
		{
			_rangeHistory.Add(_prevRange.Value);
			while (_rangeHistory.Count > CandleCount)
				try { _rangeHistory.RemoveAt(0); } catch { break; }
		}

		_prevRange = range;
		_prevHigh = high;
		_prevLow = low;
	}
}