GitHub で見る

Vortex Indicator Duplex 戦略

この戦略は、MetaTraderエキスパート Exp_VortexIndicator_Duplex をStockSharpの高レベルAPIに変換したものです。2つの独立したVortexインジケーターストリームが維持されます:1つはロングトレードを管理し、もう1つはショートトレードを管理します。各ストリームは独自の時間軸、インジケーター長、バーシフトを使用できるため、強気と弱気のセットアップ間で非対称な動作が可能です。

動作原理

  1. LongCandleTypeShortCandleType に従って2つのローソク足サブスクリプションが開かれます。各フィードは独自の VortexIndicator インスタンスを更新します。
  2. 確定したローソク足ごとに、戦略は最新のVI+とVI-の値を記録します。LongSignalBar/ShortSignalBar パラメーターは、シグナル評価に使用する確定済みローソク足の遡り本数を定義し、MetaTraderの SignalBar 入力と一致します。
  3. ロングエントリーAllowLongEntries = true の場合に許可されます。現在のロングストリームのVI+値がVI-を上回り、かつ前回のサンプル値ではVI+がVI-以下だった場合に買い注文が送信されます。新しいロングポジションを建てる前に、既存のショートエクスポージャーがすべて決済されます。
  4. ロングエグジットAllowLongExits によって有効化されます。ロングストリームのVI-値がVI+を上回るとロングポジションが決済されます。さらに、価格ステップで表した保護的なストップロスとテイクプロフィットレベル(LongStopLossStepsLongTakeProfitSteps)が各ローソク足で監視され、いずれかの閾値に達するとトレードが決済されます。
  5. ショートエントリーAllowShortEntries によって管理されます。ショートストリームのVI+が以前より上にあった後にVI-を下回ると売り注文が発注されます。既存のロングエクスポージャーは転換時に決済されます。
  6. ショートエグジットAllowShortExits によって制御されます。VI+が再びVI-を上回るとショートポジションがカバーされます。保護距離(ShortStopLossStepsShortTakeProfitSteps)に達するとトレードが決済されます。
  7. ポジションサイジングは TradeVolume パラメーターを使用します。戦略は銘柄の PriceStep を利用してステップ数を絶対価格距離に変換します。ステップパラメーターをゼロに設定すると、対応する保護ルールが無効になります。

ストップ/テイクのチェックは両方の時間軸の確定したローソク足ごとに評価されます。口座にポジションがない場合、キャッシュされたエントリーデータはMetaTrader実装を反映してクリアされます。

パラメーター

パラメーター デフォルト 説明
LongCandleType H4 ロングサイドVortexインジケーターに使用する時間軸。
ShortCandleType H4 ショートサイドインジケーターに使用する時間軸。
LongLength 14 ロングストリームに適用するVI期間。
ShortLength 14 ショートストリームに適用するVI期間。
LongSignalBar 1 ロング評価用の確定ローソク足オフセット(0 = 現在の確定バー)。
ShortSignalBar 1 ショート評価用の確定ローソク足オフセット。
AllowLongEntries true ロングポジションの建玉を有効にします。
AllowLongExits true ロングポジションの決済を有効にします。
AllowShortEntries true ショートポジションの建玉を有効にします。
AllowShortExits true ショートポジションの決済を有効にします。
LongStopLossSteps 1000 ロングトレードのストップロス距離(価格ステップ単位)。
LongTakeProfitSteps 2000 ロングトレードのテイクプロフィット距離(価格ステップ単位)。
ShortStopLossSteps 1000 ショートトレードのストップロス距離(価格ステップ単位)。
ShortTakeProfitSteps 2000 ショートトレードのテイクプロフィット距離(価格ステップ単位)。
TradeVolume 1 ポジション参入時に使用する基本市場注文サイズ。

実行上の注意

  • 戦略は新しいポジションを建てる前に反対のポジションを決済するため、個別のマジックナンバーでロングとショートのシグナルを管理していたMT5の動作を効果的に再現します。
  • 保護距離は distance = steps * Security.PriceStep で変換されます。銘柄に有効な価格ステップが設定されていることを確認してください。設定されていない場合、戦略は1.0にフォールバックします。
  • ストップ/テイクパラメーターをゼロに設定すると、シグナルベースの決済を維持しながらその保護パスを無効にできます。
  • 両方の時間軸がリスク管理をトリガーする可能性があるため、薄い市場での繰り返し転換を避けるために TradeVolume を慎重に選択してください。
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>
/// Dual Vortex indicator strategy converted from the MetaTrader expert Exp_VortexIndicator_Duplex.
/// Maintains independent long and short signal streams with configurable timeframes and risk parameters.
/// </summary>
public class VortexIndicatorDuplexStrategy : Strategy
{
	private readonly StrategyParam<DataType> _longCandleType;
	private readonly StrategyParam<DataType> _shortCandleType;
	private readonly StrategyParam<int> _longLength;
	private readonly StrategyParam<int> _shortLength;
	private readonly StrategyParam<int> _longSignalBar;
	private readonly StrategyParam<int> _shortSignalBar;
	private readonly StrategyParam<bool> _allowLongEntries;
	private readonly StrategyParam<bool> _allowLongExits;
	private readonly StrategyParam<bool> _allowShortEntries;
	private readonly StrategyParam<bool> _allowShortExits;
	private readonly StrategyParam<decimal> _longStopLossSteps;
	private readonly StrategyParam<decimal> _longTakeProfitSteps;
	private readonly StrategyParam<decimal> _shortStopLossSteps;
	private readonly StrategyParam<decimal> _shortTakeProfitSteps;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _signalCooldownBars;

	private VortexIndicator _longVortex = null!;
	private VortexIndicator _shortVortex = null!;

	private readonly List<(decimal plus, decimal minus)> _longHistory = new();
	private readonly List<(decimal plus, decimal minus)> _shortHistory = new();

	private decimal _priceStep;

	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;
	private decimal? _longStopPrice;
	private decimal? _shortStopPrice;
	private decimal? _longTakeProfitPrice;
	private decimal? _shortTakeProfitPrice;
	private int _cooldownRemaining;

	private readonly StrategyParam<int> _maxHistoryLength;

	/// <summary>
	/// Initializes parameters for the duplex Vortex strategy.
	/// </summary>
	public VortexIndicatorDuplexStrategy()
	{
		_maxHistoryLength = Param(nameof(MaxHistoryLength), 512)
			.SetGreaterThanZero()
			.SetDisplay("Max History Length", "Maximum stored Vortex samples per direction.", "General");

		_longCandleType = Param(nameof(LongCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Long Candle Type", "Timeframe used for long-side Vortex calculations.", "General");

		_shortCandleType = Param(nameof(ShortCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Short Candle Type", "Timeframe used for short-side Vortex calculations.", "General");

		_longLength = Param(nameof(LongLength), 14)
			.SetGreaterThanZero()
			.SetDisplay("Long Vortex Length", "VI period applied to the long signal stream.", "Indicator")
			
			.SetOptimize(7, 42, 7);

		_shortLength = Param(nameof(ShortLength), 14)
			.SetGreaterThanZero()
			.SetDisplay("Short Vortex Length", "VI period applied to the short signal stream.", "Indicator")
			
			.SetOptimize(7, 42, 7);

		_longSignalBar = Param(nameof(LongSignalBar), 1)
			.SetNotNegative()
			.SetDisplay("Long Signal Bar", "Closed bar shift used for long evaluations.", "Signals");

		_shortSignalBar = Param(nameof(ShortSignalBar), 1)
			.SetNotNegative()
			.SetDisplay("Short Signal Bar", "Closed bar shift used for short evaluations.", "Signals");
		_allowLongEntries = Param(nameof(AllowLongEntries), true)
			.SetDisplay("Allow Long Entries", "Enable opening long positions when VI+ crosses above VI-.", "Trading");

		_allowLongExits = Param(nameof(AllowLongExits), true)
			.SetDisplay("Allow Long Exits", "Enable closing long positions when VI- dominates VI+.", "Trading");

		_allowShortEntries = Param(nameof(AllowShortEntries), true)
			.SetDisplay("Allow Short Entries", "Enable opening short positions when VI+ crosses below VI-.", "Trading");

		_allowShortExits = Param(nameof(AllowShortExits), true)
			.SetDisplay("Allow Short Exits", "Enable closing short positions when VI+ recovers above VI-.", "Trading");

		_longStopLossSteps = Param(nameof(LongStopLossSteps), 1000m)
			.SetNotNegative()
			.SetDisplay("Long Stop Loss Steps", "Protective distance below the long entry price in price steps (0 disables).", "Risk");

		_longTakeProfitSteps = Param(nameof(LongTakeProfitSteps), 2000m)
			.SetNotNegative()
			.SetDisplay("Long Take Profit Steps", "Target distance above the long entry price in price steps (0 disables).", "Risk");

		_shortStopLossSteps = Param(nameof(ShortStopLossSteps), 1000m)
			.SetNotNegative()
			.SetDisplay("Short Stop Loss Steps", "Protective distance above the short entry price in price steps (0 disables).", "Risk");

		_shortTakeProfitSteps = Param(nameof(ShortTakeProfitSteps), 2000m)
			.SetNotNegative()
			.SetDisplay("Short Take Profit Steps", "Target distance below the short entry price in price steps (0 disables).", "Risk");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Base order volume used for entries.", "Risk");

		_signalCooldownBars = Param(nameof(SignalCooldownBars), 4)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Bars to wait between trading actions.", "Trading");
	}
	/// <summary>
	/// Candle type for the long-side signal calculations.
	/// </summary>
	public DataType LongCandleType
	{
		get => _longCandleType.Value;
		set => _longCandleType.Value = value;
	}

	/// <summary>
	/// Candle type for the short-side signal calculations.
	/// </summary>
	public DataType ShortCandleType
	{
		get => _shortCandleType.Value;
		set => _shortCandleType.Value = value;
	}

	/// <summary>
	/// Vortex period applied to the long signal stream.
	/// </summary>
	public int LongLength
	{
		get => _longLength.Value;
		set => _longLength.Value = value;
	}

	/// <summary>
	/// Vortex period applied to the short signal stream.
	/// </summary>
	public int ShortLength
	{
		get => _shortLength.Value;
		set => _shortLength.Value = value;
	}

	/// <summary>
	/// Shift in closed candles for long evaluations.
	/// </summary>
	public int LongSignalBar
	{
		get => _longSignalBar.Value;
		set => _longSignalBar.Value = value;
	}

	/// <summary>
	/// Shift in closed candles for short evaluations.
	/// </summary>
	public int ShortSignalBar
	{
		get => _shortSignalBar.Value;
		set => _shortSignalBar.Value = value;
	}
	/// <summary>
	/// Enables long entries.
	/// </summary>
	public bool AllowLongEntries
	{
		get => _allowLongEntries.Value;
		set => _allowLongEntries.Value = value;
	}

	/// <summary>
	/// Enables long exits.
	/// </summary>
	public bool AllowLongExits
	{
		get => _allowLongExits.Value;
		set => _allowLongExits.Value = value;
	}

	/// <summary>
	/// Enables short entries.
	/// </summary>
	public bool AllowShortEntries
	{
		get => _allowShortEntries.Value;
		set => _allowShortEntries.Value = value;
	}

	/// <summary>
	/// Enables short exits.
	/// </summary>
	public bool AllowShortExits
	{
		get => _allowShortExits.Value;
		set => _allowShortExits.Value = value;
	}

	/// <summary>
	/// Stop-loss distance for long trades in price steps.
	/// </summary>
	public decimal LongStopLossSteps
	{
		get => _longStopLossSteps.Value;
		set => _longStopLossSteps.Value = value;
	}

	/// <summary>
	/// Take-profit distance for long trades in price steps.
	/// </summary>
	public decimal LongTakeProfitSteps
	{
		get => _longTakeProfitSteps.Value;
		set => _longTakeProfitSteps.Value = value;
	}

	/// <summary>
	/// Stop-loss distance for short trades in price steps.
	/// </summary>
	public decimal ShortStopLossSteps
	{
		get => _shortStopLossSteps.Value;
		set => _shortStopLossSteps.Value = value;
	}

	/// <summary>
	/// Take-profit distance for short trades in price steps.
	/// </summary>
	public decimal ShortTakeProfitSteps
	{
		get => _shortTakeProfitSteps.Value;
		set => _shortTakeProfitSteps.Value = value;
	}

	/// <summary>
	/// Volume used when sending market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Bars to wait after each position change.
	/// </summary>
	public int SignalCooldownBars
	{
		get => _signalCooldownBars.Value;
		set => _signalCooldownBars.Value = value;
	}

	/// <summary>
	/// Maximum number of stored Vortex samples per signal stream.
	/// </summary>
	public int MaxHistoryLength
	{
		get => _maxHistoryLength.Value;
		set => _maxHistoryLength.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		var result = new List<(Security, DataType)> { (Security, LongCandleType) };

		if (!ShortCandleType.Equals(LongCandleType))
		{
			result.Add((Security, ShortCandleType));
		}

		return result;
	}

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

		_longHistory.Clear();
		_shortHistory.Clear();

		ResetLongState();
		ResetShortState();

		_priceStep = 0m;
		_cooldownRemaining = 0;
		_longVortex = null!;
		_shortVortex = null!;
	}
	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_priceStep = Security.PriceStep ?? 0m;
		if (_priceStep <= 0m)
		{
			_priceStep = 1m;
		}

		Volume = TradeVolume;

		_longVortex = new VortexIndicator { Length = LongLength };
		var longSubscription = SubscribeCandles(LongCandleType);
		longSubscription
			.Bind(ProcessLongCandle)
			.Start();

		_shortVortex = new VortexIndicator { Length = ShortLength };
		var shortSubscription = SubscribeCandles(ShortCandleType);
		shortSubscription
			.Bind(ProcessShortCandle)
			.Start();

		StartProtection(null, null);
	}
	private void ProcessLongCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
		{
			return;
		}

		if (_cooldownRemaining > 0)
		{
			_cooldownRemaining--;
		}

		if (CheckRiskManagement(candle.ClosePrice))
		{
			return;
		}

		var value = _longVortex.Process(candle);
		if (value is not IVortexIndicatorValue vortexValue ||
			vortexValue.PlusVi is not decimal viPlus ||
			vortexValue.MinusVi is not decimal viMinus)
		{
			return;
		}

		AppendHistory(_longHistory, (viPlus, viMinus));

		if (!_longVortex.IsFormed)
		{
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			return;
		}

		if (!TryGetHistoryPair(_longHistory, LongSignalBar, out var previous, out var current))
		{
			return;
		}

		var crossUp = previous.plus <= previous.minus && current.plus > current.minus;
		var longExit = current.minus > current.plus;

		if (longExit && AllowLongExits && Position > 0m)
		{
			SellMarket(Position);
			ResetLongState();
			_cooldownRemaining = SignalCooldownBars;
		}

		if (_cooldownRemaining == 0 && crossUp && AllowLongEntries)
		{
			TryOpenLong(candle.ClosePrice);
		}
	}
	private void ProcessShortCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
		{
			return;
		}

		if (_cooldownRemaining > 0)
		{
			_cooldownRemaining--;
		}

		if (CheckRiskManagement(candle.ClosePrice))
		{
			return;
		}

		var value = _shortVortex.Process(candle);
		if (value is not IVortexIndicatorValue vortexValue ||
			vortexValue.PlusVi is not decimal viPlus ||
			vortexValue.MinusVi is not decimal viMinus)
		{
			return;
		}

		AppendHistory(_shortHistory, (viPlus, viMinus));

		if (!_shortVortex.IsFormed)
		{
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			return;
		}

		if (!TryGetHistoryPair(_shortHistory, ShortSignalBar, out var previous, out var current))
		{
			return;
		}

		var crossDown = previous.plus >= previous.minus && current.plus < current.minus;
		var shortExit = current.plus > current.minus;

		if (shortExit && AllowShortExits && Position < 0m)
		{
			BuyMarket(-Position);
			ResetShortState();
			_cooldownRemaining = SignalCooldownBars;
		}

		if (_cooldownRemaining == 0 && crossDown && AllowShortEntries)
		{
			TryOpenShort(candle.ClosePrice);
		}
	}
	private void TryOpenLong(decimal price)
	{
		if (Position > 0m)
		{
			return;
		}

		var volume = Volume;
		if (volume <= 0m)
		{
			return;
		}

		var buyVolume = volume;
		if (Position < 0m)
		{
			buyVolume += Math.Abs(Position);
		}

		if (buyVolume <= 0m)
		{
			return;
		}

		BuyMarket(buyVolume);

		_longEntryPrice = price;
		_longStopPrice = LongStopLossSteps > 0m ? price - GetStepValue(LongStopLossSteps) : null;
		_longTakeProfitPrice = LongTakeProfitSteps > 0m ? price + GetStepValue(LongTakeProfitSteps) : null;
		_cooldownRemaining = SignalCooldownBars;

		ResetShortState();
	}
	private void TryOpenShort(decimal price)
	{
		if (Position < 0m)
		{
			return;
		}

		var volume = Volume;
		if (volume <= 0m)
		{
			return;
		}

		var sellVolume = volume;
		if (Position > 0m)
		{
			sellVolume += Position;
		}

		if (sellVolume <= 0m)
		{
			return;
		}

		SellMarket(sellVolume);

		_shortEntryPrice = price;
		_shortStopPrice = ShortStopLossSteps > 0m ? price + GetStepValue(ShortStopLossSteps) : null;
		_shortTakeProfitPrice = ShortTakeProfitSteps > 0m ? price - GetStepValue(ShortTakeProfitSteps) : null;
		_cooldownRemaining = SignalCooldownBars;

		ResetLongState();
	}
	private bool CheckRiskManagement(decimal price)
	{
		if (Position > 0m)
		{
			if (_longStopPrice is decimal stop && price <= stop)
			{
				SellMarket(Position);
				ResetLongState();
				_cooldownRemaining = SignalCooldownBars;
				return true;
			}

			if (_longTakeProfitPrice is decimal take && price >= take)
			{
				SellMarket(Position);
				ResetLongState();
				_cooldownRemaining = SignalCooldownBars;
				return true;
			}
		}
		else if (Position < 0m)
		{
			if (_shortStopPrice is decimal stop && price >= stop)
			{
				BuyMarket(-Position);
				ResetShortState();
				_cooldownRemaining = SignalCooldownBars;
				return true;
			}

			if (_shortTakeProfitPrice is decimal take && price <= take)
			{
				BuyMarket(-Position);
				ResetShortState();
				_cooldownRemaining = SignalCooldownBars;
				return true;
			}
		}
		else
		{
			ResetLongState();
			ResetShortState();
		}

		return false;
	}
	private void AppendHistory(List<(decimal plus, decimal minus)> history, (decimal plus, decimal minus) value)
	{
		history.Add(value);
		if (history.Count > MaxHistoryLength)
		{
			history.RemoveAt(0);
		}
	}

	private bool TryGetHistoryPair(List<(decimal plus, decimal minus)> history, int signalBar, out (decimal plus, decimal minus) previous, out (decimal plus, decimal minus) current)
	{
		previous = default;
		current = default;

		var currentIndex = history.Count - 1 - signalBar;
		var previousIndex = currentIndex - 1;

		if (currentIndex < 0 || previousIndex < 0)
		{
			return false;
		}

		current = history[currentIndex];
		previous = history[previousIndex];
		return true;
	}

	private decimal GetStepValue(decimal steps)
	{
		return steps * _priceStep;
	}

	private void ResetLongState()
	{
		_longEntryPrice = null;
		_longStopPrice = null;
		_longTakeProfitPrice = null;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = null;
		_shortStopPrice = null;
		_shortTakeProfitPrice = null;
	}
}