Auf GitHub ansehen

VLT Trader Filter-Strategie

Übersicht

Die VLT Trader Filter-Strategie ist ein Volatilitätskontraktions-Ausbruchssystem, das aus der ursprünglichen MQL-Implementierung konvertiert wurde. Es überwacht aktuelle Kerzenspannen und bereitet Stop-Orders vor, wann immer die zuletzt abgeschlossene Kerze zur kleinsten Spanne in einem konfigurierbaren historischen Fenster wird. Ziel ist es, explosive Bewegungen nach einer engen Konsolidierungsphase zu erfassen.

Handelslogik

  1. Neue Balkenverarbeitung – die Strategie wertet Bedingungen nur einmal pro neuer Kerze aus. Die aktuelle Kerze muss unter dem Hoch der vorherigen Kerze öffnen, um das Handeln von Gaps zu vermeiden, die durch das Ausbruchsniveau springen.
  2. Volatilitätsfilter – die Spanne der zuletzt abgeschlossenen Kerze wird mit der kleinsten Spanne unter den letzten CandleCount abgeschlossenen Kerzen verglichen, deren Spanne unter MaxCandleSizePips liegt. Wenn die jüngste Kerze strikt kleiner ist, ist das Setup gültig.
  3. Einstiegsplatzierung – wenn das Setup gültig ist, werden zwei Stop-Orders vorbereitet:
    • Ein Buy Stop 10 Pips über dem vorherigen Hoch, wenn die Nettoposition nicht long ist.
    • Ein Sell Stop 10 Pips unter dem vorherigen Tief, wenn die Nettoposition nicht short ist. Bestehende ausstehende Orders desselben Typs werden vor dem Registrieren neuer storniert.
  4. Risikomanagement – sobald eine Stop-Order ausgelöst wird und eine Position eröffnet, werden automatisch Schutzorders angehängt:
    • Take-Profit bei TakeProfitPips über/unter dem Einstandspreis.
    • Stop-Loss bei StopLossPips unter/über dem Einstandspreis. Schutzorders werden storniert, wenn die Position auf null zurückkehrt.

Parameter

Parameter Beschreibung
Volume Mit jeder Stop-Order gesendetes Ordervolumen.
TakeProfitPips Abstand in Pips für die Take-Profit-Order nach dem Einstieg.
StopLossPips Abstand in Pips für den Schutz-Stop nach dem Einstieg.
MaxCandleSizePips Obergrenze für die historischen Kerzenspannen, die im Volatilitätsfilter berücksichtigt werden.
CandleCount Anzahl der historischen Kerzen zur Suche nach der minimalen akzeptablen Spanne.
CandleType Für die Analyse verwendeter Kerzen-Zeitrahmen.

Implementierungshinweise

  • Die Pip-Größe wird vom Preisschritt des Instruments abgeleitet. Wenn der Schritt kleiner oder gleich 0.001 ist, wird er mit 10 multipliziert, um die MetaTrader-Pip-Definition für 3- oder 5-Dezimalinstrumente zu emulieren.
  • Kerzenspannen werden in einer FIFO-Warteschlange mit maximal CandleCount Elementen gespeichert, was dem historischen Scanning des ursprünglichen Expert Advisors entspricht.
  • Alle Orders werden über die High-Level-StockSharp-API erstellt (keine manuelle Order-Registrierung) und automatisch storniert, wenn sie veraltet sind oder die Position schließt.
  • Kommentare im Code sind auf Englisch verfasst, während README-Dateien ausführliche mehrsprachige Dokumentation bereitstellen.
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;
	}
}