GitHub で見る

VLT Trader フィルター戦略

概要

VLT Trader フィルター戦略は、オリジナルのMQL実装から変換されたボラティリティ収縮ブレイクアウトシステムです。最近のローソク足のレンジを監視し、最新の完成したローソク足が設定可能な履歴ウィンドウ内で最小のレンジになるたびにストップ注文を準備します。目標は、タイトな統合期間の後に爆発的な動きを捉えることです。

トレードロジック

  1. 新バーの処理 – 戦略は新しいローソク足ごとに1回だけ条件を評価します。現在のローソク足はブレイクアウトレベルを跳び越えるギャップの取引を避けるために、前のローソク足の高値より下で始まる必要があります。
  2. ボラティリティフィルター – 最新の完成したローソク足のレンジは、MaxCandleSizePips 以下のレンジを持つ最後の CandleCount 本の完成したローソク足の中の最小レンジと比較されます。最新のローソク足が厳密に小さい場合、セットアップは有効です。
  3. エントリーの配置 – セットアップが有効な場合、2つのストップ注文が準備されます:
    • ネットポジションがロングでないとき、前の高値の 10 pips上に buy stop
    • ネットポジションがショートでないとき、前の安値の 10 pips下に sell stop。 同じタイプの既存のペンディング注文は新しいものを登録する前にキャンセルされます。
  4. リスク管理 – ストップ注文がトリガーされてポジションが開かれると、保護注文が自動的に添付されます:
    • エントリー価格の TakeProfitPips 上/下にテイクプロフィット。
    • エントリー価格の StopLossPips 下/上にストップロス。 保護注文はポジションがゼロに戻ったときにキャンセルされます。

パラメーター

パラメーター 説明
Volume 各ストップ注文で送信される注文ボリューム。
TakeProfitPips エントリー後のテイクプロフィット注文に使用するpips単位の距離。
StopLossPips エントリー後の保護ストップに使用するpips単位の距離。
MaxCandleSizePips ボラティリティフィルターで考慮される履歴ローソク足レンジの上限。
CandleCount 最小の許容可能なレンジを見つけるために使用する履歴ローソク足の数。
CandleType 分析に使用するローソク足の時間軸。

実装に関する注意事項

  • pip サイズは楽器の価格ステップから導出されます。ステップが 0.001 以下の場合、3桁または5桁小数の楽器に対するMetaTraderのpip定義を模倣するために 10 倍されます。
  • ローソク足のレンジは CandleCount 要素に制限されたFIFOキューに保存され、オリジナルのExpert Advisorで実行された履歴スキャンに一致します。
  • すべての注文はStockSharpの高レベルAPIを通じて作成され(手動の注文登録なし)、古くなったり、ポジションが閉じたりすると自動的にキャンセルされます。
  • コード内のコメントは英語で書かれており、READMEファイルは詳細な多言語ドキュメントを提供します。
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;
	}
}